diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
new file mode 100644
index 0000000..83b18b0
--- /dev/null
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -0,0 +1,35 @@
+name: "Copilot Setup Steps"
+
+# Automatically run the setup steps when they are changed to allow for easy validation, and
+# allow manual testing through the repository's "Actions" tab
+on:
+ workflow_dispatch:
+ push:
+ paths:
+ - .github/workflows/copilot-setup-steps.yml
+
+jobs:
+ # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
+ copilot-setup-steps:
+ runs-on: ubuntu-latest
+
+ # Set the permissions to the lowest permissions possible needed for your steps.
+ # Copilot will be given its own token for its operations.
+ permissions:
+ # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
+ contents: read
+
+ # You can define any steps you want, and they will run before the agent starts.
+ # If you do not check out your code, Copilot will do this for you.
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: "npm"
+ - name: Install dependencies
+ run: npm install
+ - name: Enable pre-commit hooks
+ run: npm run prepare
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f450893..dd9d6ca 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,7 +1,7 @@
name: Release DYA Studio
on:
- workflow_dispatch:
+ workflow_dispatch:
jobs:
build:
@@ -27,4 +27,4 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_RELEASE }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID_RELEASE }}
environment: release
- wranglerVersion: '4.58.0'
+ wranglerVersion: "4.58.0"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a2640b5..e91b3ac 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -86,7 +86,7 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: versions upload --assets dist --name dya-studio --compatibility-date 2026-01-11
- wranglerVersion: '4.58.0'
+ wranglerVersion: "4.58.0"
- name: Comment PR with deployment URL
uses: actions/github-script@v7
@@ -123,4 +123,4 @@ jobs:
issue_number: context.issue.number,
body: comment
});
- }
\ No newline at end of file
+ }
diff --git a/.gitignore b/.gitignore
index ee4e5e1..12bb520 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@ coverage
*.njsproj
*.sln
*.sw?
+_codeql_detected_source_root
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..1c0ebca
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,2 @@
+npx lint-staged
+npm test
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..be4ad67
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,8 @@
+dist
+node_modules
+coverage
+*.min.js
+*.min.css
+pnpm-lock.yaml
+package-lock.json
+src/proto
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..8daa855
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,10 @@
+{
+ "semi": true,
+ "trailingComma": "all",
+ "singleQuote": false,
+ "printWidth": 80,
+ "tabWidth": 2,
+ "useTabs": false,
+ "arrowParens": "always",
+ "endOfLine": "lf"
+}
diff --git a/docs/PRECOMMIT_SETUP.md b/docs/PRECOMMIT_SETUP.md
new file mode 100644
index 0000000..d298d71
--- /dev/null
+++ b/docs/PRECOMMIT_SETUP.md
@@ -0,0 +1,56 @@
+# Pre-commit Hooks Setup
+
+This project uses [Husky](https://typicode.github.io/husky/) and [lint-staged](https://github.com/lint-staged/lint-staged) to ensure code quality before commits.
+
+## What runs on pre-commit?
+
+1. **Formatting**: Prettier formats staged files (`.ts`, `.tsx`, `.json`, `.md`, `.yml`, `.yaml`)
+2. **Linting**: ESLint checks and auto-fixes staged TypeScript files
+3. **Testing**: Full test suite runs to ensure nothing is broken
+
+## How it works
+
+When you run `git commit`, the pre-commit hook automatically:
+
+- Runs `lint-staged` to format and lint only the staged files
+- Runs `npm test` to verify all tests pass
+- If any step fails, the commit is aborted
+
+## Manual commands
+
+You can also run these checks manually:
+
+```bash
+# Format all files
+npm run format
+
+# Check formatting without changing files
+npm run format:check
+
+# Run linting
+npm run lint
+
+# Run tests
+npm test
+```
+
+## First-time setup
+
+If you clone this repository, run:
+
+```bash
+npm install
+```
+
+This will automatically set up Husky hooks via the `prepare` script.
+
+## GitHub Actions
+
+The project includes a Copilot workflow (`.github/workflows/copilot.yml`) that runs:
+
+- Format check
+- Lint
+- Tests
+- Build
+
+This ensures all code quality checks pass in CI/CD.
diff --git a/eslint.config.js b/eslint.config.js
index 5e6b472..75d3c46 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,14 +1,14 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
-import { defineConfig, globalIgnores } from 'eslint/config'
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
- globalIgnores(['dist']),
+ globalIgnores(["dist"]),
{
- files: ['**/*.{ts,tsx}'],
+ files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
@@ -20,4 +20,4 @@ export default defineConfig([
globals: globals.browser,
},
},
-])
+]);
diff --git a/package-lock.json b/package-lock.json
index a7732d6..bdd71ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,10 +42,13 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
+ "husky": "^9.1.7",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^30.2.0",
+ "lint-staged": "^16.2.7",
"postcss": "^8.5.6",
+ "prettier": "^3.8.1",
"run-script-os": "^1.1.6",
"tailwindcss": "^4.1.18",
"ts-jest": "^29.4.6",
@@ -5477,6 +5480,85 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cli-cursor": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
+ "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^7.1.0",
+ "string-width": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/string-width": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz",
+ "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -5539,6 +5621,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -6033,6 +6132,19 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
@@ -6544,6 +6656,19 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-east-asian-width": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
+ "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -6758,6 +6883,22 @@
"node": ">=10.17.0"
}
},
+ "node_modules/husky": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
+ "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "husky": "bin.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -9372,6 +9513,134 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lint-staged": {
+ "version": "16.2.7",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz",
+ "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^14.0.2",
+ "listr2": "^9.0.5",
+ "micromatch": "^4.0.8",
+ "nano-spawn": "^2.0.0",
+ "pidtree": "^0.6.0",
+ "string-argv": "^0.3.2",
+ "yaml": "^2.8.1"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/listr2": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
+ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cli-truncate": "^5.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/listr2/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -9402,6 +9671,127 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/log-update": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-escapes": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
+ "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-update/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -9539,6 +9929,19 @@
"node": ">=6"
}
},
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -9594,6 +9997,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nano-spawn": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
+ "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -9893,6 +10309,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@@ -10018,6 +10447,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@@ -10382,6 +10827,59 @@
"node": ">=10"
}
},
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/rolldown": {
"version": "1.0.0-beta.50",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.50.tgz",
@@ -10523,6 +11021,52 @@
"node": ">=8"
}
},
+ "node_modules/slice-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
+ "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+ "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
@@ -10595,6 +11139,16 @@
"node": ">=8"
}
},
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
"node_modules/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@@ -11548,6 +12102,22 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yaml": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
+ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
diff --git a/package.json b/package.json
index c484784..1cb1158 100644
--- a/package.json
+++ b/package.json
@@ -7,11 +7,14 @@
"dev": "npm run generate && vite",
"build": "npm run generate && tsc -b && vite build",
"lint": "eslint .",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
"preview": "vite preview",
"generate": "buf generate",
"test": "jest",
"test:watch": "jest --watch",
- "test:coverage": "jest --coverage"
+ "test:coverage": "jest --coverage",
+ "prepare": "husky"
},
"dependencies": {
"@cormoran/zmk-studio-react-hook": "github:cormoran/react-zmk-studio",
@@ -48,10 +51,13 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
+ "husky": "^9.1.7",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^30.2.0",
+ "lint-staged": "^16.2.7",
"postcss": "^8.5.6",
+ "prettier": "^3.8.1",
"run-script-os": "^1.1.6",
"tailwindcss": "^4.1.18",
"ts-jest": "^29.4.6",
@@ -62,6 +68,15 @@
"vite": "npm:rolldown-vite@7.2.5",
"vite-plugin-svgr": "^4.5.0"
},
+ "lint-staged": {
+ "*.{ts,tsx}": [
+ "prettier --write",
+ "eslint --fix"
+ ],
+ "*.{json,md,yml,yaml}": [
+ "prettier --write"
+ ]
+ },
"pnpm": {
"overrides": {
"vite": "npm:rolldown-vite@7.2.5"
diff --git a/src/components/BatteryHistoryChart.tsx b/src/components/BatteryHistoryChart.tsx
index 9afce7b..c1ef64f 100644
--- a/src/components/BatteryHistoryChart.tsx
+++ b/src/components/BatteryHistoryChart.tsx
@@ -36,7 +36,7 @@ interface RestartMarker {
function formatTimestamp(timestamp: number): string {
const hours = Math.floor(timestamp / 3600);
const minutes = Math.floor((timestamp % 3600) / 60);
-
+
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
@@ -54,7 +54,8 @@ function detectRestarts(devices: DeviceBatteryHistory[]): RestartMarker[] {
const currTimestamp = device.entries[i].timestamp;
// Detect restart: timestamp goes backwards or resets to a very small value
- const isRestart = currTimestamp < prevTimestamp || currTimestamp < ONE_HOUR_IN_SECONDS;
+ const isRestart =
+ currTimestamp < prevTimestamp || currTimestamp < ONE_HOUR_IN_SECONDS;
if (isRestart && !seenTimestamps.has(currTimestamp)) {
restarts.push({ timestamp: currTimestamp });
@@ -95,7 +96,11 @@ function combineDeviceData(devices: DeviceBatteryHistory[]): ChartDataPoint[] {
}
// Custom tooltip component
-function CustomTooltip({ active, payload, label }: {
+function CustomTooltip({
+ active,
+ payload,
+ label,
+}: {
active?: boolean;
payload?: Array<{ name: string; value: number; color: string }>;
label?: string;
@@ -103,10 +108,13 @@ function CustomTooltip({ active, payload, label }: {
if (active && payload && payload.length) {
return (
-
{label}
+
+ {label}
+
{payload.map((entry, index) => (
- ● {entry.name}: {entry.value}%
+ ● {entry.name}:{" "}
+ {entry.value}%
))}
@@ -115,7 +123,10 @@ function CustomTooltip({ active, payload, label }: {
return null;
}
-export function BatteryHistoryChart({ devices, deviceColors }: BatteryHistoryChartProps) {
+export function BatteryHistoryChart({
+ devices,
+ deviceColors,
+}: BatteryHistoryChartProps) {
const chartData = useMemo(() => combineDeviceData(devices), [devices]);
const restartMarkers = useMemo(() => detectRestarts(devices), [devices]);
@@ -176,7 +187,7 @@ export function BatteryHistoryChart({ devices, deviceColors }: BatteryHistoryCha
color: "var(--color-text-secondary)",
}}
/>
-
+
{/* Restart markers */}
{restartMarkers.map((marker, index) => (
);
}
-
diff --git a/src/components/SplashScreen.tsx b/src/components/SplashScreen.tsx
index 47d32c4..541186d 100644
--- a/src/components/SplashScreen.tsx
+++ b/src/components/SplashScreen.tsx
@@ -170,7 +170,7 @@ export function SplashScreen({
)}
-
+
{/* Demo mode hint */}
- Try{" "}
- demo mode without
- a device
+ Try demo mode{" "}
+ without a device
diff --git a/src/components/UnlockPrompt.tsx b/src/components/UnlockPrompt.tsx
index 9ce81a1..27b9c7a 100644
--- a/src/components/UnlockPrompt.tsx
+++ b/src/components/UnlockPrompt.tsx
@@ -36,8 +36,8 @@ export function UnlockPrompt({ open, onClose, onRetry }: UnlockPromptProps) {
{/* Description */}
- Your keyboard's ZMK Studio is locked. Please unlock it to continue
- editing your keymap.
+ Your keyboard's ZMK Studio is locked. Please unlock it to
+ continue editing your keymap.
{/* Instructions */}
diff --git a/src/hooks/__tests__/useBatteryHistory.test.tsx b/src/hooks/__tests__/useBatteryHistory.test.tsx
index 9271b33..aa22765 100644
--- a/src/hooks/__tests__/useBatteryHistory.test.tsx
+++ b/src/hooks/__tests__/useBatteryHistory.test.tsx
@@ -74,12 +74,17 @@ describe("useBatteryHistory", () => {
await result.current.loadBatteryHistory();
});
- expect(result.current.error).toBe("Not connected to device or subsystem not found");
+ expect(result.current.error).toBe(
+ "Not connected to device or subsystem not found",
+ );
});
it("should set error when subsystem not found", async () => {
const wrapper = createWrapper({
- state: { connection: { isConnected: true } as never, customSubsystems: [] },
+ state: {
+ connection: { isConnected: true } as never,
+ customSubsystems: [],
+ },
findSubsystem: () => null,
onNotification: mockOnNotification,
});
@@ -90,7 +95,9 @@ describe("useBatteryHistory", () => {
await result.current.loadBatteryHistory();
});
- expect(result.current.error).toBe("Not connected to device or subsystem not found");
+ expect(result.current.error).toBe(
+ "Not connected to device or subsystem not found",
+ );
});
});
@@ -105,7 +112,9 @@ describe("useBatteryHistory", () => {
customSubsystems: [],
},
findSubsystem: (id: string) =>
- id === "zmk__battery_history" ? { index: 0, identifier: "zmk__battery_history" } : null,
+ id === "zmk__battery_history"
+ ? { index: 0, identifier: "zmk__battery_history" }
+ : null,
onNotification: mockOnNotification,
});
@@ -125,7 +134,7 @@ describe("useBatteryHistory", () => {
// Mock successful clear response
mockCallRPC.mockResolvedValue(
- new Uint8Array([18, 2, 8, 10]) // ClearBatteryHistoryResponse with entriesCleared: 10
+ new Uint8Array([18, 2, 8, 10]), // ClearBatteryHistoryResponse with entriesCleared: 10
);
const wrapper = createWrapper({
@@ -134,7 +143,9 @@ describe("useBatteryHistory", () => {
customSubsystems: [{ index: 0, identifier: "zmk__battery_history" }],
},
findSubsystem: (id: string) =>
- id === "zmk__battery_history" ? { index: 0, identifier: "zmk__battery_history" } : null,
+ id === "zmk__battery_history"
+ ? { index: 0, identifier: "zmk__battery_history" }
+ : null,
onNotification: mockOnNotification,
});
@@ -160,7 +171,9 @@ describe("useBatteryHistory", () => {
customSubsystems: [{ index: 0, identifier: "zmk__battery_history" }],
},
findSubsystem: (id: string) =>
- id === "zmk__battery_history" ? { index: 0, identifier: "zmk__battery_history" } : null,
+ id === "zmk__battery_history"
+ ? { index: 0, identifier: "zmk__battery_history" }
+ : null,
onNotification: mockOnNotification,
});
diff --git a/src/hooks/__tests__/useKeymap.test.tsx b/src/hooks/__tests__/useKeymap.test.tsx
index b03bf2d..e7baf28 100644
--- a/src/hooks/__tests__/useKeymap.test.tsx
+++ b/src/hooks/__tests__/useKeymap.test.tsx
@@ -75,7 +75,10 @@ const mockKeymap = {
const mockBehaviors = [1, 2, 3]; // kp, trans, mo
-const mockBehaviorDetails: Record = {
+const mockBehaviorDetails: Record<
+ number,
+ { id: number; displayName: string; metadata: never[] }
+> = {
1: { id: 1, displayName: "kp", metadata: [] },
2: { id: 2, displayName: "trans", metadata: [] },
3: { id: 3, displayName: "mo", metadata: [] },
@@ -129,17 +132,23 @@ describe("useKeymap", () => {
// Setup mock responses
mockCallRpc.mockImplementation(async (_conn, req) => {
if (req.keymap?.getPhysicalLayouts) {
- return { keymap: { getPhysicalLayouts: mockPhysicalLayouts } } as never;
+ return {
+ keymap: { getPhysicalLayouts: mockPhysicalLayouts },
+ } as never;
}
if (req.keymap?.getKeymap) {
return { keymap: { getKeymap: mockKeymap } } as never;
}
if (req.behaviors?.listAllBehaviors) {
- return { behaviors: { listAllBehaviors: { behaviors: mockBehaviors } } } as never;
+ return {
+ behaviors: { listAllBehaviors: { behaviors: mockBehaviors } },
+ } as never;
}
if (req.behaviors?.getBehaviorDetails) {
const id = req.behaviors.getBehaviorDetails.behaviorId;
- return { behaviors: { getBehaviorDetails: mockBehaviorDetails[id] } } as never;
+ return {
+ behaviors: { getBehaviorDetails: mockBehaviorDetails[id] },
+ } as never;
}
if (req.keymap?.checkUnsavedChanges) {
return { keymap: { checkUnsavedChanges: false } } as never;
@@ -195,17 +204,23 @@ describe("useKeymap", () => {
// Setup mock responses for successful loading
mockCallRpc.mockImplementation(async (_conn, req) => {
if (req.keymap?.getPhysicalLayouts) {
- return { keymap: { getPhysicalLayouts: mockPhysicalLayouts } } as never;
+ return {
+ keymap: { getPhysicalLayouts: mockPhysicalLayouts },
+ } as never;
}
if (req.keymap?.getKeymap) {
return { keymap: { getKeymap: mockKeymap } } as never;
}
if (req.behaviors?.listAllBehaviors) {
- return { behaviors: { listAllBehaviors: { behaviors: mockBehaviors } } } as never;
+ return {
+ behaviors: { listAllBehaviors: { behaviors: mockBehaviors } },
+ } as never;
}
if (req.behaviors?.getBehaviorDetails) {
const id = req.behaviors.getBehaviorDetails.behaviorId;
- return { behaviors: { getBehaviorDetails: mockBehaviorDetails[id] } } as never;
+ return {
+ behaviors: { getBehaviorDetails: mockBehaviorDetails[id] },
+ } as never;
}
if (req.keymap?.checkUnsavedChanges) {
return { keymap: { checkUnsavedChanges: false } } as never;
@@ -235,17 +250,23 @@ describe("useKeymap", () => {
mockCallRpc.mockImplementation(async (_conn, req) => {
if (req.keymap?.getPhysicalLayouts) {
- return { keymap: { getPhysicalLayouts: mockPhysicalLayouts } } as never;
+ return {
+ keymap: { getPhysicalLayouts: mockPhysicalLayouts },
+ } as never;
}
if (req.keymap?.getKeymap) {
return { keymap: { getKeymap: mockKeymap } } as never;
}
if (req.behaviors?.listAllBehaviors) {
- return { behaviors: { listAllBehaviors: { behaviors: mockBehaviors } } } as never;
+ return {
+ behaviors: { listAllBehaviors: { behaviors: mockBehaviors } },
+ } as never;
}
if (req.behaviors?.getBehaviorDetails) {
const id = req.behaviors.getBehaviorDetails.behaviorId;
- return { behaviors: { getBehaviorDetails: mockBehaviorDetails[id] } } as never;
+ return {
+ behaviors: { getBehaviorDetails: mockBehaviorDetails[id] },
+ } as never;
}
if (req.keymap?.checkUnsavedChanges) {
return { keymap: { checkUnsavedChanges: false } } as never;
@@ -276,17 +297,23 @@ describe("useKeymap", () => {
mockCallRpc.mockImplementation(async (_conn, req) => {
if (req.keymap?.getPhysicalLayouts) {
- return { keymap: { getPhysicalLayouts: mockPhysicalLayouts } } as never;
+ return {
+ keymap: { getPhysicalLayouts: mockPhysicalLayouts },
+ } as never;
}
if (req.keymap?.getKeymap) {
return { keymap: { getKeymap: mockKeymap } } as never;
}
if (req.behaviors?.listAllBehaviors) {
- return { behaviors: { listAllBehaviors: { behaviors: mockBehaviors } } } as never;
+ return {
+ behaviors: { listAllBehaviors: { behaviors: mockBehaviors } },
+ } as never;
}
if (req.behaviors?.getBehaviorDetails) {
const id = req.behaviors.getBehaviorDetails.behaviorId;
- return { behaviors: { getBehaviorDetails: mockBehaviorDetails[id] } } as never;
+ return {
+ behaviors: { getBehaviorDetails: mockBehaviorDetails[id] },
+ } as never;
}
if (req.keymap?.checkUnsavedChanges) {
return { keymap: { checkUnsavedChanges: false } } as never;
diff --git a/src/hooks/__tests__/useRuntimeInputProcessor.test.tsx b/src/hooks/__tests__/useRuntimeInputProcessor.test.tsx
index 9b34b1d..08da2ad 100644
--- a/src/hooks/__tests__/useRuntimeInputProcessor.test.tsx
+++ b/src/hooks/__tests__/useRuntimeInputProcessor.test.tsx
@@ -8,7 +8,10 @@ import { renderHook, act } from "@testing-library/react";
import { useRuntimeInputProcessor } from "../useRuntimeInputProcessor";
import { ZMKAppContext } from "@cormoran/zmk-studio-react-hook";
import type { ReactNode } from "react";
-import { Response, Notification } from "../../proto/zmk/runtime_input_processor/runtime_input_processor";
+import {
+ Response,
+ Notification,
+} from "../../proto/zmk/runtime_input_processor/runtime_input_processor";
// Mock ZMKCustomSubsystem
const mockCallRPC = jest.fn();
@@ -53,7 +56,9 @@ describe("useRuntimeInputProcessor", () => {
onNotification: mockOnNotification,
});
- const { result } = renderHook(() => useRuntimeInputProcessor(), { wrapper });
+ const { result } = renderHook(() => useRuntimeInputProcessor(), {
+ wrapper,
+ });
expect(result.current.processors).toEqual([]);
expect(result.current.isLoading).toBe(false);
@@ -69,29 +74,40 @@ describe("useRuntimeInputProcessor", () => {
onNotification: mockOnNotification,
});
- const { result } = renderHook(() => useRuntimeInputProcessor(), { wrapper });
+ const { result } = renderHook(() => useRuntimeInputProcessor(), {
+ wrapper,
+ });
await act(async () => {
await result.current.loadProcessors();
});
- expect(result.current.error).toBe("Not connected to device or subsystem not found");
+ expect(result.current.error).toBe(
+ "Not connected to device or subsystem not found",
+ );
});
it("should set error when subsystem not found", async () => {
const wrapper = createWrapper({
- state: { connection: { isConnected: true } as never, customSubsystems: [] },
+ state: {
+ connection: { isConnected: true } as never,
+ customSubsystems: [],
+ },
findSubsystem: () => null,
onNotification: mockOnNotification,
});
- const { result } = renderHook(() => useRuntimeInputProcessor(), { wrapper });
+ const { result } = renderHook(() => useRuntimeInputProcessor(), {
+ wrapper,
+ });
await act(async () => {
await result.current.loadProcessors();
});
- expect(result.current.error).toBe("Not connected to device or subsystem not found");
+ expect(result.current.error).toBe(
+ "Not connected to device or subsystem not found",
+ );
});
});
@@ -100,11 +116,15 @@ describe("useRuntimeInputProcessor", () => {
const mockConnection = { isConnected: true };
// Mock notification callback
- let notificationCallback: ((notification: { payload: Uint8Array }) => void) | null = null;
- mockOnNotification.mockImplementation((subscription: { callback: typeof notificationCallback }) => {
- notificationCallback = subscription.callback;
- return () => {}; // unsubscribe function
- });
+ let notificationCallback:
+ | ((notification: { payload: Uint8Array }) => void)
+ | null = null;
+ mockOnNotification.mockImplementation(
+ (subscription: { callback: typeof notificationCallback }) => {
+ notificationCallback = subscription.callback;
+ return () => {}; // unsubscribe function
+ },
+ );
// Mock successful RPC response (empty, data comes via notification)
const response = Response.create({
@@ -115,18 +135,24 @@ describe("useRuntimeInputProcessor", () => {
const wrapper = createWrapper({
state: {
connection: mockConnection as never,
- customSubsystems: [{ index: 0, identifier: "zmk__runtime_input_processor" }],
+ customSubsystems: [
+ { index: 0, identifier: "zmk__runtime_input_processor" },
+ ],
},
findSubsystem: (id: string) =>
- id === "zmk__runtime_input_processor" ? { index: 0, identifier: "zmk__runtime_input_processor" } : null,
+ id === "zmk__runtime_input_processor"
+ ? { index: 0, identifier: "zmk__runtime_input_processor" }
+ : null,
onNotification: mockOnNotification,
});
- const { result } = renderHook(() => useRuntimeInputProcessor(), { wrapper });
+ const { result } = renderHook(() => useRuntimeInputProcessor(), {
+ wrapper,
+ });
// Wait for useEffect to trigger loadProcessors
await act(async () => {
- await new Promise(resolve => setTimeout(resolve, 50));
+ await new Promise((resolve) => setTimeout(resolve, 50));
});
// Simulate notification arrival
@@ -142,8 +168,10 @@ describe("useRuntimeInputProcessor", () => {
},
});
await act(async () => {
- notificationCallback({ payload: Notification.encode(notification).finish() });
- await new Promise(resolve => setTimeout(resolve, 600)); // Wait for notification collection timeout
+ notificationCallback({
+ payload: Notification.encode(notification).finish(),
+ });
+ await new Promise((resolve) => setTimeout(resolve, 600)); // Wait for notification collection timeout
});
}
@@ -167,11 +195,15 @@ describe("useRuntimeInputProcessor", () => {
customSubsystems: [],
},
findSubsystem: (id: string) =>
- id === "zmk__runtime_input_processor" ? { index: 0, identifier: "zmk__runtime_input_processor" } : null,
+ id === "zmk__runtime_input_processor"
+ ? { index: 0, identifier: "zmk__runtime_input_processor" }
+ : null,
onNotification: mockOnNotification,
});
- const { result } = renderHook(() => useRuntimeInputProcessor(), { wrapper });
+ const { result } = renderHook(() => useRuntimeInputProcessor(), {
+ wrapper,
+ });
// Verify the functions exist
expect(typeof result.current.loadProcessors).toBe("function");
@@ -186,23 +218,27 @@ describe("useRuntimeInputProcessor", () => {
it("should set scaling successfully and simplify fraction", async () => {
const mockConnection = { isConnected: true };
- let notificationCallback: ((notification: { payload: Uint8Array }) => void) | null = null;
- mockOnNotification.mockImplementation((subscription: { callback: typeof notificationCallback }) => {
- notificationCallback = subscription.callback;
- return () => {};
- });
+ let notificationCallback:
+ | ((notification: { payload: Uint8Array }) => void)
+ | null = null;
+ mockOnNotification.mockImplementation(
+ (subscription: { callback: typeof notificationCallback }) => {
+ notificationCallback = subscription.callback;
+ return () => {};
+ },
+ );
// Mock successful initial load
const initialLoadResponse = Response.create({ listProcessors: {} });
-
+
// Mock successful set scaling response
const setScalingResponse = Response.create({
setScaling: { success: true },
});
-
+
// Mock reload after setting
const reloadResponse = Response.create({ listProcessors: {} });
-
+
mockCallRPC
.mockResolvedValueOnce(Response.encode(initialLoadResponse).finish())
.mockResolvedValueOnce(Response.encode(setScalingResponse).finish())
@@ -211,18 +247,24 @@ describe("useRuntimeInputProcessor", () => {
const wrapper = createWrapper({
state: {
connection: mockConnection as never,
- customSubsystems: [{ index: 0, identifier: "zmk__runtime_input_processor" }],
+ customSubsystems: [
+ { index: 0, identifier: "zmk__runtime_input_processor" },
+ ],
},
findSubsystem: (id: string) =>
- id === "zmk__runtime_input_processor" ? { index: 0, identifier: "zmk__runtime_input_processor" } : null,
+ id === "zmk__runtime_input_processor"
+ ? { index: 0, identifier: "zmk__runtime_input_processor" }
+ : null,
onNotification: mockOnNotification,
});
- const { result } = renderHook(() => useRuntimeInputProcessor(), { wrapper });
+ const { result } = renderHook(() => useRuntimeInputProcessor(), {
+ wrapper,
+ });
// Wait for initial load and send initial notification
await act(async () => {
- await new Promise(resolve => setTimeout(resolve, 50));
+ await new Promise((resolve) => setTimeout(resolve, 50));
if (notificationCallback) {
const initialNotification = Notification.create({
processorSettings: {
@@ -234,9 +276,11 @@ describe("useRuntimeInputProcessor", () => {
},
},
});
- notificationCallback({ payload: Notification.encode(initialNotification).finish() });
+ notificationCallback({
+ payload: Notification.encode(initialNotification).finish(),
+ });
}
- await new Promise(resolve => setTimeout(resolve, 600));
+ await new Promise((resolve) => setTimeout(resolve, 600));
});
// Now call setScaling with a value that can be simplified (200/100 => 2/1)
@@ -254,9 +298,11 @@ describe("useRuntimeInputProcessor", () => {
},
},
});
- notificationCallback({ payload: Notification.encode(updatedNotification).finish() });
+ notificationCallback({
+ payload: Notification.encode(updatedNotification).finish(),
+ });
}
- await new Promise(resolve => setTimeout(resolve, 600));
+ await new Promise((resolve) => setTimeout(resolve, 600));
});
expect(result.current.error).toBe(null);
@@ -269,23 +315,27 @@ describe("useRuntimeInputProcessor", () => {
it("should set rotation successfully", async () => {
const mockConnection = { isConnected: true };
- let notificationCallback: ((notification: { payload: Uint8Array }) => void) | null = null;
- mockOnNotification.mockImplementation((subscription: { callback: typeof notificationCallback }) => {
- notificationCallback = subscription.callback;
- return () => {};
- });
+ let notificationCallback:
+ | ((notification: { payload: Uint8Array }) => void)
+ | null = null;
+ mockOnNotification.mockImplementation(
+ (subscription: { callback: typeof notificationCallback }) => {
+ notificationCallback = subscription.callback;
+ return () => {};
+ },
+ );
// Mock successful initial load
const initialLoadResponse = Response.create({ listProcessors: {} });
-
+
// Mock successful set rotation response
const setRotationResponse = Response.create({
setRotation: { success: true },
});
-
+
// Mock reload after setting
const reloadResponse = Response.create({ listProcessors: {} });
-
+
mockCallRPC
.mockResolvedValueOnce(Response.encode(initialLoadResponse).finish())
.mockResolvedValueOnce(Response.encode(setRotationResponse).finish())
@@ -294,18 +344,24 @@ describe("useRuntimeInputProcessor", () => {
const wrapper = createWrapper({
state: {
connection: mockConnection as never,
- customSubsystems: [{ index: 0, identifier: "zmk__runtime_input_processor" }],
+ customSubsystems: [
+ { index: 0, identifier: "zmk__runtime_input_processor" },
+ ],
},
findSubsystem: (id: string) =>
- id === "zmk__runtime_input_processor" ? { index: 0, identifier: "zmk__runtime_input_processor" } : null,
+ id === "zmk__runtime_input_processor"
+ ? { index: 0, identifier: "zmk__runtime_input_processor" }
+ : null,
onNotification: mockOnNotification,
});
- const { result } = renderHook(() => useRuntimeInputProcessor(), { wrapper });
+ const { result } = renderHook(() => useRuntimeInputProcessor(), {
+ wrapper,
+ });
// Wait for initial load and send initial notification
await act(async () => {
- await new Promise(resolve => setTimeout(resolve, 50));
+ await new Promise((resolve) => setTimeout(resolve, 50));
if (notificationCallback) {
const initialNotification = Notification.create({
processorSettings: {
@@ -317,9 +373,11 @@ describe("useRuntimeInputProcessor", () => {
},
},
});
- notificationCallback({ payload: Notification.encode(initialNotification).finish() });
+ notificationCallback({
+ payload: Notification.encode(initialNotification).finish(),
+ });
}
- await new Promise(resolve => setTimeout(resolve, 600));
+ await new Promise((resolve) => setTimeout(resolve, 600));
});
// Now call setRotation
@@ -337,9 +395,11 @@ describe("useRuntimeInputProcessor", () => {
},
},
});
- notificationCallback({ payload: Notification.encode(updatedNotification).finish() });
+ notificationCallback({
+ payload: Notification.encode(updatedNotification).finish(),
+ });
}
- await new Promise(resolve => setTimeout(resolve, 600));
+ await new Promise((resolve) => setTimeout(resolve, 600));
});
expect(result.current.error).toBe(null);
@@ -356,14 +416,20 @@ describe("useRuntimeInputProcessor", () => {
const wrapper = createWrapper({
state: {
connection: mockConnection as never,
- customSubsystems: [{ index: 0, identifier: "zmk__runtime_input_processor" }],
+ customSubsystems: [
+ { index: 0, identifier: "zmk__runtime_input_processor" },
+ ],
},
findSubsystem: (id: string) =>
- id === "zmk__runtime_input_processor" ? { index: 0, identifier: "zmk__runtime_input_processor" } : null,
+ id === "zmk__runtime_input_processor"
+ ? { index: 0, identifier: "zmk__runtime_input_processor" }
+ : null,
onNotification: mockOnNotification,
});
- const { result } = renderHook(() => useRuntimeInputProcessor(), { wrapper });
+ const { result } = renderHook(() => useRuntimeInputProcessor(), {
+ wrapper,
+ });
await act(async () => {
await result.current.loadProcessors();
@@ -384,14 +450,20 @@ describe("useRuntimeInputProcessor", () => {
const wrapper = createWrapper({
state: {
connection: mockConnection as never,
- customSubsystems: [{ index: 0, identifier: "zmk__runtime_input_processor" }],
+ customSubsystems: [
+ { index: 0, identifier: "zmk__runtime_input_processor" },
+ ],
},
findSubsystem: (id: string) =>
- id === "zmk__runtime_input_processor" ? { index: 0, identifier: "zmk__runtime_input_processor" } : null,
+ id === "zmk__runtime_input_processor"
+ ? { index: 0, identifier: "zmk__runtime_input_processor" }
+ : null,
onNotification: mockOnNotification,
});
- const { result } = renderHook(() => useRuntimeInputProcessor(), { wrapper });
+ const { result } = renderHook(() => useRuntimeInputProcessor(), {
+ wrapper,
+ });
await act(async () => {
await result.current.loadProcessors();
diff --git a/src/hooks/useBLEProfiles.ts b/src/hooks/useBLEProfiles.ts
index f0cef2d..0ce0e36 100644
--- a/src/hooks/useBLEProfiles.ts
+++ b/src/hooks/useBLEProfiles.ts
@@ -1,5 +1,8 @@
import { useState, useEffect, useCallback, useContext, useMemo } from "react";
-import { ZMKCustomSubsystem, ZMKAppContext } from "@cormoran/zmk-studio-react-hook";
+import {
+ ZMKCustomSubsystem,
+ ZMKAppContext,
+} from "@cormoran/zmk-studio-react-hook";
import {
Request,
Response,
@@ -39,15 +42,16 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
const [maxProfiles, setMaxProfiles] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
- const [outputPriority, setOutputPriorityState] = useState(null);
+ const [outputPriority, setOutputPriorityState] =
+ useState(null);
// Memoize subsystem to avoid unnecessary re-renders
const subsystem = useMemo(
() => zmkApp?.findSubsystem(SUBSYSTEM_IDENTIFIER),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [zmkApp?.state.customSubsystems]
+ [zmkApp?.state.customSubsystems],
);
-
+
// Extract subsystem index as a stable primitive value for dependencies
const subsystemIndex = subsystem?.index;
@@ -63,7 +67,7 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
const request = Request.create({
@@ -94,7 +98,7 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
} catch (err) {
console.error("Failed to load profiles:", err);
setError(
- `Failed to load profiles: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to load profiles: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
@@ -111,7 +115,7 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
const request = Request.create({
@@ -134,13 +138,13 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
} catch (err) {
console.error("Failed to switch profile:", err);
setError(
- `Failed to switch profile: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to switch profile: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
}
},
- [zmkApp?.state.connection, subsystemIndex, loadProfiles]
+ [zmkApp?.state.connection, subsystemIndex, loadProfiles],
);
const unpairProfile = useCallback(
@@ -153,7 +157,7 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
const request = Request.create({
@@ -176,13 +180,13 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
} catch (err) {
console.error("Failed to unpair profile:", err);
setError(
- `Failed to unpair profile: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to unpair profile: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
}
},
- [zmkApp?.state.connection, subsystemIndex, loadProfiles]
+ [zmkApp?.state.connection, subsystemIndex, loadProfiles],
);
const setProfileName = useCallback(
@@ -195,7 +199,7 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
const request = Request.create({
@@ -218,13 +222,13 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
} catch (err) {
console.error("Failed to set profile name:", err);
setError(
- `Failed to set profile name: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to set profile name: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
}
},
- [zmkApp?.state.connection, subsystemIndex, loadProfiles]
+ [zmkApp?.state.connection, subsystemIndex, loadProfiles],
);
const getOutputPriority = useCallback(async () => {
@@ -239,7 +243,7 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
const request = Request.create({
@@ -260,7 +264,7 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
} catch (err) {
console.error("Failed to get output priority:", err);
setError(
- `Failed to get output priority: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to get output priority: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
@@ -277,7 +281,7 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
const request = Request.create({
@@ -300,13 +304,13 @@ export function useBLEProfiles(): UseBLEProfilesReturn {
} catch (err) {
console.error("Failed to set output priority:", err);
setError(
- `Failed to set output priority: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to set output priority: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
}
},
- [zmkApp?.state.connection, subsystemIndex, getOutputPriority]
+ [zmkApp?.state.connection, subsystemIndex, getOutputPriority],
);
// Load profiles and output priority when connection or subsystem changes
diff --git a/src/hooks/useBatteryHistory.ts b/src/hooks/useBatteryHistory.ts
index 45109de..295a6ac 100644
--- a/src/hooks/useBatteryHistory.ts
+++ b/src/hooks/useBatteryHistory.ts
@@ -1,5 +1,8 @@
import { useState, useEffect, useCallback, useContext, useMemo } from "react";
-import { ZMKCustomSubsystem, ZMKAppContext } from "@cormoran/zmk-studio-react-hook";
+import {
+ ZMKCustomSubsystem,
+ ZMKAppContext,
+} from "@cormoran/zmk-studio-react-hook";
import {
Request,
Response,
@@ -39,7 +42,7 @@ export function useBatteryHistory(): UseBatteryHistoryReturn {
const subsystem = useMemo(
() => zmkApp?.findSubsystem(SUBSYSTEM_IDENTIFIER),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [zmkApp?.state.customSubsystems]
+ [zmkApp?.state.customSubsystems],
);
// Extract subsystem index as a stable primitive value for dependencies
@@ -57,20 +60,23 @@ export function useBatteryHistory(): UseBatteryHistoryReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
// Map to store entries by source ID
const deviceMap = new Map();
// Set up notification listener for battery history entries
- const notificationHandler = (notification: BatteryHistoryNotification) => {
+ const notificationHandler = (
+ notification: BatteryHistoryNotification,
+ ) => {
try {
const { sourceId, entry } = notification;
// Initialize device if not exists
if (!deviceMap.has(sourceId)) {
- const deviceName = sourceId === 0 ? "Central" : `Peripheral ${sourceId}`;
+ const deviceName =
+ sourceId === 0 ? "Central" : `Peripheral ${sourceId}`;
deviceMap.set(sourceId, {
sourceId,
deviceName,
@@ -102,12 +108,17 @@ export function useBatteryHistory(): UseBatteryHistoryReturn {
callback: (customNotification) => {
// Decode the payload
try {
- const notification = Notification.decode(customNotification.payload);
+ const notification = Notification.decode(
+ customNotification.payload,
+ );
if (notification.batteryHistory) {
notificationHandler(notification.batteryHistory);
}
} catch (err) {
- console.error("Failed to decode battery history notification:", err);
+ console.error(
+ "Failed to decode battery history notification:",
+ err,
+ );
}
},
});
@@ -136,7 +147,7 @@ export function useBatteryHistory(): UseBatteryHistoryReturn {
} catch (err) {
console.error("Failed to load battery history:", err);
setError(
- `Failed to load battery history: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to load battery history: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
@@ -152,7 +163,7 @@ export function useBatteryHistory(): UseBatteryHistoryReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
const request = Request.create({
@@ -175,7 +186,7 @@ export function useBatteryHistory(): UseBatteryHistoryReturn {
} catch (err) {
console.error("Failed to clear battery history:", err);
setError(
- `Failed to clear battery history: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to clear battery history: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
diff --git a/src/hooks/useRuntimeInputProcessor.ts b/src/hooks/useRuntimeInputProcessor.ts
index 013f474..ad02472 100644
--- a/src/hooks/useRuntimeInputProcessor.ts
+++ b/src/hooks/useRuntimeInputProcessor.ts
@@ -1,5 +1,8 @@
import { useState, useEffect, useCallback, useContext, useMemo } from "react";
-import { ZMKCustomSubsystem, ZMKAppContext } from "@cormoran/zmk-studio-react-hook";
+import {
+ ZMKCustomSubsystem,
+ ZMKAppContext,
+} from "@cormoran/zmk-studio-react-hook";
import {
Request,
Response,
@@ -27,7 +30,10 @@ function gcd(a: number, b: number): number {
}
// Helper function to simplify a fraction to lowest terms
-function simplifyFraction(multiplier: number, divisor: number): { multiplier: number; divisor: number } {
+function simplifyFraction(
+ multiplier: number,
+ divisor: number,
+): { multiplier: number; divisor: number } {
if (divisor === 0) return { multiplier, divisor };
const divisorValue = gcd(multiplier, divisor);
return {
@@ -48,7 +54,11 @@ export interface UseRuntimeInputProcessorReturn {
isLoading: boolean;
error: string | null;
loadProcessors: () => Promise;
- setScaling: (name: string, multiplier: number, divisor: number) => Promise;
+ setScaling: (
+ name: string,
+ multiplier: number,
+ divisor: number,
+ ) => Promise;
setRotation: (name: string, degrees: number) => Promise;
}
@@ -62,7 +72,7 @@ export function useRuntimeInputProcessor(): UseRuntimeInputProcessorReturn {
const subsystem = useMemo(
() => zmkApp?.findSubsystem(SUBSYSTEM_IDENTIFIER),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [zmkApp?.state.customSubsystems]
+ [zmkApp?.state.customSubsystems],
);
// Extract subsystem index as a stable primitive value for dependencies
@@ -80,7 +90,7 @@ export function useRuntimeInputProcessor(): UseRuntimeInputProcessorReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
// Map to store processors by name
@@ -110,7 +120,9 @@ export function useRuntimeInputProcessor(): UseRuntimeInputProcessorReturn {
callback: (customNotification) => {
// Decode the payload
try {
- const notification = Notification.decode(customNotification.payload);
+ const notification = Notification.decode(
+ customNotification.payload,
+ );
if (notification.processorSettings?.processor) {
notificationHandler(notification.processorSettings.processor);
}
@@ -138,13 +150,15 @@ export function useRuntimeInputProcessor(): UseRuntimeInputProcessorReturn {
}
} finally {
// Wait for all notifications to arrive from devices
- await new Promise((resolve) => setTimeout(resolve, NOTIFICATION_COLLECTION_TIMEOUT_MS));
+ await new Promise((resolve) =>
+ setTimeout(resolve, NOTIFICATION_COLLECTION_TIMEOUT_MS),
+ );
unsubscribe();
}
} catch (err) {
console.error("Failed to load processors:", err);
setError(
- `Failed to load processors: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to load processors: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
@@ -161,7 +175,7 @@ export function useRuntimeInputProcessor(): UseRuntimeInputProcessorReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
// Simplify the fraction to reduce risk of overflow
@@ -191,13 +205,13 @@ export function useRuntimeInputProcessor(): UseRuntimeInputProcessorReturn {
} catch (err) {
console.error("Failed to set scaling:", err);
setError(
- `Failed to set scaling: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to set scaling: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
}
},
- [zmkApp?.state.connection, subsystemIndex, loadProcessors]
+ [zmkApp?.state.connection, subsystemIndex, loadProcessors],
);
const setRotation = useCallback(
@@ -210,7 +224,7 @@ export function useRuntimeInputProcessor(): UseRuntimeInputProcessorReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
const request = Request.create({
@@ -236,13 +250,13 @@ export function useRuntimeInputProcessor(): UseRuntimeInputProcessorReturn {
} catch (err) {
console.error("Failed to set rotation:", err);
setError(
- `Failed to set rotation: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to set rotation: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
}
},
- [zmkApp?.state.connection, subsystemIndex, loadProcessors]
+ [zmkApp?.state.connection, subsystemIndex, loadProcessors],
);
// Load processors when connection or subsystem changes
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index ec6e2e8..3bd6704 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -1,5 +1,8 @@
import { useState, useEffect, useCallback, useContext, useMemo } from "react";
-import { ZMKCustomSubsystem, ZMKAppContext } from "@cormoran/zmk-studio-react-hook";
+import {
+ ZMKCustomSubsystem,
+ ZMKAppContext,
+} from "@cormoran/zmk-studio-react-hook";
import {
Request,
Response,
@@ -39,7 +42,7 @@ export function useSettings(): UseSettingsReturn {
const subsystem = useMemo(
() => zmkApp?.findSubsystem(SUBSYSTEM_IDENTIFIER),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [zmkApp?.state.customSubsystems]
+ [zmkApp?.state.customSubsystems],
);
// Extract subsystem index as a stable primitive value for dependencies
@@ -57,7 +60,7 @@ export function useSettings(): UseSettingsReturn {
try {
const service = new ZMKCustomSubsystem(
zmkApp.state.connection,
- subsystemIndex
+ subsystemIndex,
);
// Map to store settings by source ID
@@ -67,8 +70,9 @@ export function useSettings(): UseSettingsReturn {
const notificationHandler = (settings: ActivitySettings) => {
try {
const sourceId = settings.source;
- const deviceName = sourceId === 0 ? "Central" : `Peripheral ${sourceId}`;
-
+ const deviceName =
+ sourceId === 0 ? "Central" : `Peripheral ${sourceId}`;
+
deviceMap.set(sourceId, {
sourceId,
deviceName,
@@ -79,7 +83,10 @@ export function useSettings(): UseSettingsReturn {
// Update state with all collected devices
setDevices(Array.from(deviceMap.values()));
} catch (err) {
- console.error("Failed to process activity settings notification:", err);
+ console.error(
+ "Failed to process activity settings notification:",
+ err,
+ );
}
};
@@ -90,7 +97,9 @@ export function useSettings(): UseSettingsReturn {
callback: (customNotification) => {
// Decode the payload
try {
- const notification = Notification.decode(customNotification.payload);
+ const notification = Notification.decode(
+ customNotification.payload,
+ );
if (notification.activitySettings?.settings) {
notificationHandler(notification.activitySettings.settings);
}
@@ -118,65 +127,70 @@ export function useSettings(): UseSettingsReturn {
}
} finally {
// Wait for all notifications to arrive from devices
- await new Promise((resolve) => setTimeout(resolve, NOTIFICATION_COLLECTION_TIMEOUT_MS));
+ await new Promise((resolve) =>
+ setTimeout(resolve, NOTIFICATION_COLLECTION_TIMEOUT_MS),
+ );
unsubscribe();
}
} catch (err) {
console.error("Failed to load settings:", err);
setError(
- `Failed to load settings: ${err instanceof Error ? err.message : "Unknown error"}`
+ `Failed to load settings: ${err instanceof Error ? err.message : "Unknown error"}`,
);
} finally {
setIsLoading(false);
}
}, [zmkApp, subsystemIndex]);
- const setActivitySettings = useCallback(async (idleMs: number, sleepMs: number) => {
- if (!zmkApp?.state.connection || subsystemIndex === undefined) {
- setError("Not connected to device or subsystem not found");
- return;
- }
+ const setActivitySettings = useCallback(
+ async (idleMs: number, sleepMs: number) => {
+ if (!zmkApp?.state.connection || subsystemIndex === undefined) {
+ setError("Not connected to device or subsystem not found");
+ return;
+ }
- setIsLoading(true);
- setError(null);
+ setIsLoading(true);
+ setError(null);
- try {
- const service = new ZMKCustomSubsystem(
- zmkApp.state.connection,
- subsystemIndex
- );
+ try {
+ const service = new ZMKCustomSubsystem(
+ zmkApp.state.connection,
+ subsystemIndex,
+ );
- const request = Request.create({
- setActivitySettings: {
- settings: {
- idleMs,
- sleepMs,
- source: 0, // Not used for set operation
+ const request = Request.create({
+ setActivitySettings: {
+ settings: {
+ idleMs,
+ sleepMs,
+ source: 0, // Not used for set operation
+ },
},
- },
- });
+ });
- const payload = Request.encode(request).finish();
- const responsePayload = await service.callRPC(payload);
+ const payload = Request.encode(request).finish();
+ const responsePayload = await service.callRPC(payload);
- if (responsePayload) {
- const resp = Response.decode(responsePayload);
- if (resp.error) {
- setError(resp.error.message);
- } else if (resp.setActivitySettings?.success) {
- // Successfully set, reload settings
- await loadAllSettings();
+ if (responsePayload) {
+ const resp = Response.decode(responsePayload);
+ if (resp.error) {
+ setError(resp.error.message);
+ } else if (resp.setActivitySettings?.success) {
+ // Successfully set, reload settings
+ await loadAllSettings();
+ }
}
+ } catch (err) {
+ console.error("Failed to set activity settings:", err);
+ setError(
+ `Failed to set activity settings: ${err instanceof Error ? err.message : "Unknown error"}`,
+ );
+ } finally {
+ setIsLoading(false);
}
- } catch (err) {
- console.error("Failed to set activity settings:", err);
- setError(
- `Failed to set activity settings: ${err instanceof Error ? err.message : "Unknown error"}`
- );
- } finally {
- setIsLoading(false);
- }
- }, [zmkApp, subsystemIndex, loadAllSettings]);
+ },
+ [zmkApp, subsystemIndex, loadAllSettings],
+ );
const resetToDefaults = useCallback(async () => {
// Reset to ZMK default values:
diff --git a/src/index.css b/src/index.css
index 238262d..e501f1b 100644
--- a/src/index.css
+++ b/src/index.css
@@ -60,7 +60,11 @@
}
html {
- font-family: "Inter", system-ui, -apple-system, sans-serif;
+ font-family:
+ "Inter",
+ system-ui,
+ -apple-system,
+ sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -72,7 +76,9 @@
color: var(--color-text);
min-height: 100vh;
overflow-x: hidden;
- transition: background-color 0.3s ease, color 0.3s ease;
+ transition:
+ background-color 0.3s ease,
+ color 0.3s ease;
}
#root {
@@ -108,7 +114,9 @@
border-radius: 0.75rem;
border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
- transition: background-color 0.3s ease, border-color 0.3s ease;
+ transition:
+ background-color 0.3s ease,
+ border-color 0.3s ease;
}
:root.light .glass-card {
@@ -123,7 +131,9 @@
);
backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
- transition: background 0.3s ease, border-color 0.3s ease;
+ transition:
+ background 0.3s ease,
+ border-color 0.3s ease;
}
.btn-electric {
@@ -147,7 +157,9 @@
.btn-electric:hover {
background: rgba(0, 212, 255, 0.3);
border-color: rgba(0, 212, 255, 0.6);
- box-shadow: 0 0 20px rgba(0, 212, 255, 0.4), 0 0 40px rgba(0, 212, 255, 0.2);
+ box-shadow:
+ 0 0 20px rgba(0, 212, 255, 0.4),
+ 0 0 40px rgba(0, 212, 255, 0.2);
}
:root.light .btn-electric:hover {
@@ -187,7 +199,9 @@
.btn-neon:hover {
background: rgba(0, 255, 204, 0.3);
border-color: rgba(0, 255, 204, 0.6);
- box-shadow: 0 0 20px rgba(0, 255, 204, 0.4), 0 0 40px rgba(0, 255, 204, 0.2);
+ box-shadow:
+ 0 0 20px rgba(0, 255, 204, 0.4),
+ 0 0 40px rgba(0, 255, 204, 0.2);
}
:root.light .btn-neon:hover {
@@ -334,19 +348,27 @@
/* Utility classes */
.text-glow-electric {
- text-shadow: 0 0 10px rgba(0, 212, 255, 0.5), 0 0 20px rgba(0, 212, 255, 0.3);
+ text-shadow:
+ 0 0 10px rgba(0, 212, 255, 0.5),
+ 0 0 20px rgba(0, 212, 255, 0.3);
}
:root.light .text-glow-electric {
- text-shadow: 0 0 10px rgba(0, 153, 204, 0.3), 0 0 20px rgba(0, 153, 204, 0.15);
+ text-shadow:
+ 0 0 10px rgba(0, 153, 204, 0.3),
+ 0 0 20px rgba(0, 153, 204, 0.15);
}
.text-glow-neon {
- text-shadow: 0 0 10px rgba(0, 255, 204, 0.5), 0 0 20px rgba(0, 255, 204, 0.3);
+ text-shadow:
+ 0 0 10px rgba(0, 255, 204, 0.5),
+ 0 0 20px rgba(0, 255, 204, 0.3);
}
:root.light .text-glow-neon {
- text-shadow: 0 0 10px rgba(0, 184, 148, 0.3), 0 0 20px rgba(0, 184, 148, 0.15);
+ text-shadow:
+ 0 0 10px rgba(0, 184, 148, 0.3),
+ 0 0 20px rgba(0, 184, 148, 0.15);
}
.bg-gradient-dark {
diff --git a/src/lib/transport/__tests__/demo-battery.test.ts b/src/lib/transport/__tests__/demo-battery.test.ts
index ea5dfbf..0b5bfe7 100644
--- a/src/lib/transport/__tests__/demo-battery.test.ts
+++ b/src/lib/transport/__tests__/demo-battery.test.ts
@@ -3,7 +3,10 @@
*/
import { BatteryHistoryHandler } from "../demo-battery";
-import { Request, Notification } from "../../../proto/zmk/battery_history/battery_history";
+import {
+ Request,
+ Notification,
+} from "../../../proto/zmk/battery_history/battery_history";
describe("BatteryHistoryHandler", () => {
let handler: BatteryHistoryHandler;
@@ -26,7 +29,7 @@ describe("BatteryHistoryHandler", () => {
it("should send battery history notifications via callback", (done) => {
const notifications: Notification[] = [];
-
+
handler.notify((payload: Uint8Array) => {
const notification = Notification.decode(payload);
notifications.push(notification);
@@ -42,9 +45,11 @@ describe("BatteryHistoryHandler", () => {
setTimeout(() => {
// Should have notifications for both central and peripheral
expect(notifications.length).toBeGreaterThan(0);
-
+
// Check that we have notifications from different sources
- const sourceIds = new Set(notifications.map(n => n.batteryHistory?.sourceId));
+ const sourceIds = new Set(
+ notifications.map((n) => n.batteryHistory?.sourceId),
+ );
expect(sourceIds.has(0)).toBe(true); // Central
expect(sourceIds.has(1)).toBe(true); // Peripheral
@@ -52,12 +57,20 @@ describe("BatteryHistoryHandler", () => {
const firstNotification = notifications[0];
expect(firstNotification.batteryHistory).toBeDefined();
expect(firstNotification.batteryHistory?.entry).toBeDefined();
- expect(firstNotification.batteryHistory?.entry?.timestamp).toBeGreaterThan(0);
- expect(firstNotification.batteryHistory?.entry?.batteryLevel).toBeGreaterThanOrEqual(0);
- expect(firstNotification.batteryHistory?.entry?.batteryLevel).toBeLessThanOrEqual(100);
+ expect(
+ firstNotification.batteryHistory?.entry?.timestamp,
+ ).toBeGreaterThan(0);
+ expect(
+ firstNotification.batteryHistory?.entry?.batteryLevel,
+ ).toBeGreaterThanOrEqual(0);
+ expect(
+ firstNotification.batteryHistory?.entry?.batteryLevel,
+ ).toBeLessThanOrEqual(100);
// Verify isLast flag is set correctly
- const lastNotifications = notifications.filter(n => n.batteryHistory?.isLast);
+ const lastNotifications = notifications.filter(
+ (n) => n.batteryHistory?.isLast,
+ );
expect(lastNotifications.length).toBeGreaterThan(0);
done();
@@ -93,7 +106,7 @@ describe("BatteryHistoryHandler", () => {
describe("notify callback", () => {
it("should register notify callback", (done) => {
let callbackCalled = false;
-
+
handler.notify(() => {
callbackCalled = true;
});
diff --git a/src/lib/transport/__tests__/demo-runtime-input-processor.test.ts b/src/lib/transport/__tests__/demo-runtime-input-processor.test.ts
index a7ebbcf..0053943 100644
--- a/src/lib/transport/__tests__/demo-runtime-input-processor.test.ts
+++ b/src/lib/transport/__tests__/demo-runtime-input-processor.test.ts
@@ -3,7 +3,10 @@
*/
import { RuntimeInputProcessorHandler } from "../demo-runtime-input-processor";
-import { Request, Notification } from "../../../proto/zmk/runtime_input_processor/runtime_input_processor";
+import {
+ Request,
+ Notification,
+} from "../../../proto/zmk/runtime_input_processor/runtime_input_processor";
describe("RuntimeInputProcessorHandler", () => {
let handler: RuntimeInputProcessorHandler;
@@ -28,7 +31,7 @@ describe("RuntimeInputProcessorHandler", () => {
it("should send processor notifications via callback", (done) => {
const notifications: Notification[] = [];
-
+
handler.notify((payload: Uint8Array) => {
const notification = Notification.decode(payload);
notifications.push(notification);
@@ -44,15 +47,23 @@ describe("RuntimeInputProcessorHandler", () => {
setTimeout(() => {
// Should have at least one processor notification
expect(notifications.length).toBeGreaterThan(0);
-
+
// Verify notification structure
const firstNotification = notifications[0];
expect(firstNotification.processorSettings).toBeDefined();
expect(firstNotification.processorSettings?.processor).toBeDefined();
- expect(firstNotification.processorSettings?.processor?.name).toBeDefined();
- expect(firstNotification.processorSettings?.processor?.scaleMultiplier).toBeDefined();
- expect(firstNotification.processorSettings?.processor?.scaleDivisor).toBeDefined();
- expect(firstNotification.processorSettings?.processor?.rotationDegrees).toBeDefined();
+ expect(
+ firstNotification.processorSettings?.processor?.name,
+ ).toBeDefined();
+ expect(
+ firstNotification.processorSettings?.processor?.scaleMultiplier,
+ ).toBeDefined();
+ expect(
+ firstNotification.processorSettings?.processor?.scaleDivisor,
+ ).toBeDefined();
+ expect(
+ firstNotification.processorSettings?.processor?.rotationDegrees,
+ ).toBeDefined();
done();
}, 300);
@@ -108,7 +119,7 @@ describe("RuntimeInputProcessorHandler", () => {
it("should update processor scaling and send notification", (done) => {
const notifications: Notification[] = [];
-
+
handler.notify((payload: Uint8Array) => {
const notification = Notification.decode(payload);
notifications.push(notification);
@@ -127,9 +138,11 @@ describe("RuntimeInputProcessorHandler", () => {
// Wait for notification to be sent
setTimeout(() => {
expect(notifications.length).toBeGreaterThan(0);
-
+
const notification = notifications[0];
- expect(notification.processorSettings?.processor?.scaleMultiplier).toBe(2);
+ expect(notification.processorSettings?.processor?.scaleMultiplier).toBe(
+ 2,
+ );
expect(notification.processorSettings?.processor?.scaleDivisor).toBe(1);
done();
@@ -170,7 +183,7 @@ describe("RuntimeInputProcessorHandler", () => {
it("should update processor rotation and send notification", (done) => {
const notifications: Notification[] = [];
-
+
handler.notify((payload: Uint8Array) => {
const notification = Notification.decode(payload);
notifications.push(notification);
@@ -188,9 +201,11 @@ describe("RuntimeInputProcessorHandler", () => {
// Wait for notification to be sent
setTimeout(() => {
expect(notifications.length).toBeGreaterThan(0);
-
+
const notification = notifications[0];
- expect(notification.processorSettings?.processor?.rotationDegrees).toBe(90);
+ expect(notification.processorSettings?.processor?.rotationDegrees).toBe(
+ 90,
+ );
done();
}, 200);
@@ -225,7 +240,7 @@ describe("RuntimeInputProcessorHandler", () => {
describe("notify callback", () => {
it("should register notify callback", (done) => {
let callbackCalled = false;
-
+
handler.notify(() => {
callbackCalled = true;
});
diff --git a/src/lib/transport/__tests__/demo.test.ts b/src/lib/transport/__tests__/demo.test.ts
index 5655e55..3447790 100644
--- a/src/lib/transport/__tests__/demo.test.ts
+++ b/src/lib/transport/__tests__/demo.test.ts
@@ -12,7 +12,7 @@ describe("Demo Transport", () => {
// Skip stream tests in Node.js environment since TransformStream is browser-only
it.skip("should create a valid RpcTransport", async () => {
const transport = await connect();
-
+
expect(transport).toBeDefined();
expect(transport.label).toBe("Demo");
expect(transport.abortController).toBeInstanceOf(AbortController);
diff --git a/src/lib/transport/demo-battery.ts b/src/lib/transport/demo-battery.ts
index 28ac242..d756894 100644
--- a/src/lib/transport/demo-battery.ts
+++ b/src/lib/transport/demo-battery.ts
@@ -27,7 +27,7 @@ function generateBatteryHistory(
// Different starting battery levels for variety
const startLevel = sourceId === 0 ? 85 : 78;
-
+
// Generate entries going back in time
for (let i = 0; i < count; i++) {
const timestamp = now - (count - i - 1) * intervalSeconds;
@@ -37,7 +37,7 @@ function generateBatteryHistory(
const variation = Math.sin(i * 0.3) * 2; // Small periodic variation
const batteryLevel = Math.max(
20,
- Math.min(100, startLevel - i * drainRate + variation)
+ Math.min(100, startLevel - i * drainRate + variation),
);
entries.push({
@@ -62,12 +62,15 @@ export class BatteryHistoryHandler {
process(request: Request): Response {
if (request.getHistory !== undefined) {
// Send battery history via notifications
- const sendHistoryForDevice = (sourceId: number, entries: BatteryHistoryEntry[]) => {
+ const sendHistoryForDevice = (
+ sourceId: number,
+ entries: BatteryHistoryEntry[],
+ ) => {
const totalEntries = entries.length;
-
+
entries.forEach((entry, index) => {
const isLast = index === entries.length - 1;
-
+
this.callbacks.forEach((cb) => {
cb(
Notification.encode({
@@ -78,7 +81,7 @@ export class BatteryHistoryHandler {
totalEntries,
entryIndex: index,
},
- }).finish()
+ }).finish(),
);
});
});
@@ -100,18 +103,18 @@ export class BatteryHistoryHandler {
if (request.clearHistory !== undefined) {
// Calculate total entries before clearing
- const totalEntries =
- this.batteryHistory.central.length +
+ const totalEntries =
+ this.batteryHistory.central.length +
this.batteryHistory.peripheral.length;
-
+
// Clear history and regenerate fresh data
this.batteryHistory.central = generateBatteryHistory(0, 48);
this.batteryHistory.peripheral = generateBatteryHistory(1, 48);
-
- return {
- clearHistory: {
- entriesCleared: totalEntries
- }
+
+ return {
+ clearHistory: {
+ entriesCleared: totalEntries,
+ },
};
}
diff --git a/src/lib/transport/demo-runtime-input-processor.ts b/src/lib/transport/demo-runtime-input-processor.ts
index 09c8c70..a2b65d1 100644
--- a/src/lib/transport/demo-runtime-input-processor.ts
+++ b/src/lib/transport/demo-runtime-input-processor.ts
@@ -11,7 +11,8 @@ import {
type ProcessorInfo,
} from "../../proto/zmk/runtime_input_processor/runtime_input_processor";
-export const RUNTIME_INPUT_PROCESSOR_IDENTIFIER = "zmk__runtime_input_processor";
+export const RUNTIME_INPUT_PROCESSOR_IDENTIFIER =
+ "zmk__runtime_input_processor";
/**
* Mock runtime input processor data
@@ -32,7 +33,7 @@ const MOCK_PROCESSORS: ProcessorInfo[] = [
export class RuntimeInputProcessorHandler {
private callbacks: ((data: Uint8Array) => void)[] = [];
private processors: ProcessorInfo[] = JSON.parse(
- JSON.stringify(MOCK_PROCESSORS)
+ JSON.stringify(MOCK_PROCESSORS),
);
process(request: Request): Response {
@@ -47,7 +48,7 @@ export class RuntimeInputProcessorHandler {
processorSettings: {
processor,
},
- }).finish()
+ }).finish(),
);
});
});
@@ -59,22 +60,22 @@ export class RuntimeInputProcessorHandler {
if (request.getProcessor !== undefined) {
const { name } = request.getProcessor;
const processor = this.processors.find((p) => p.name === name);
-
+
if (processor) {
return { getProcessor: { processor } };
}
-
+
return { error: { message: `Processor not found: ${name}` } };
}
if (request.setScaling !== undefined) {
const { name, scaleMultiplier, scaleDivisor } = request.setScaling;
const processor = this.processors.find((p) => p.name === name);
-
+
if (processor) {
processor.scaleMultiplier = scaleMultiplier;
processor.scaleDivisor = scaleDivisor;
-
+
// Send notification about the update
setTimeout(() => {
console.log("Demo sending updated processor settings:", processor);
@@ -84,24 +85,24 @@ export class RuntimeInputProcessorHandler {
processorSettings: {
processor,
},
- }).finish()
+ }).finish(),
);
});
}, 50);
-
+
return { setScaling: { success: true } };
}
-
+
return { setScaling: { success: false } };
}
if (request.setRotation !== undefined) {
const { name, rotationDegrees } = request.setRotation;
const processor = this.processors.find((p) => p.name === name);
-
+
if (processor) {
processor.rotationDegrees = rotationDegrees;
-
+
// Send notification about the update
setTimeout(() => {
console.log("Demo sending updated processor settings:", processor);
@@ -111,14 +112,14 @@ export class RuntimeInputProcessorHandler {
processorSettings: {
processor,
},
- }).finish()
+ }).finish(),
);
});
}, 50);
-
+
return { setRotation: { success: true } };
}
-
+
return { setRotation: { success: false } };
}
diff --git a/src/lib/transport/demo.ts b/src/lib/transport/demo.ts
index e5173aa..3a73388 100644
--- a/src/lib/transport/demo.ts
+++ b/src/lib/transport/demo.ts
@@ -13,8 +13,14 @@ import {
} from "@zmkfirmware/zmk-studio-ts-client";
import { BLEManagementHandler, BLE_MANAGEMENT_IDENTIFIER } from "./demo-ble";
import { SettingsHandler, SETTINGS_IDENTIFIER } from "./demo-settings";
-import { BatteryHistoryHandler, BATTERY_HISTORY_IDENTIFIER } from "./demo-battery";
-import { RuntimeInputProcessorHandler, RUNTIME_INPUT_PROCESSOR_IDENTIFIER } from "./demo-runtime-input-processor";
+import {
+ BatteryHistoryHandler,
+ BATTERY_HISTORY_IDENTIFIER,
+} from "./demo-battery";
+import {
+ RuntimeInputProcessorHandler,
+ RUNTIME_INPUT_PROCESSOR_IDENTIFIER,
+} from "./demo-runtime-input-processor";
import {
Request as BLERequest,
Response as BLEResponse,
@@ -223,12 +229,16 @@ class Keyboard {
} catch (e) {
console.error("Battery History subsystem error:", e);
}
- } else if (subsystemIndex === this.RUNTIME_INPUT_PROCESSOR_SUBSYSTEM_INDEX) {
+ } else if (
+ subsystemIndex === this.RUNTIME_INPUT_PROCESSOR_SUBSYSTEM_INDEX
+ ) {
// Runtime Input Processor
try {
const runtimeReq = RuntimeInputProcessorRequest.decode(data);
- const runtimeResp = this.runtimeInputProcessorHandler.process(runtimeReq);
- responseData = RuntimeInputProcessorResponse.encode(runtimeResp).finish();
+ const runtimeResp =
+ this.runtimeInputProcessorHandler.process(runtimeReq);
+ responseData =
+ RuntimeInputProcessorResponse.encode(runtimeResp).finish();
} catch (e) {
console.error("Runtime Input Processor subsystem error:", e);
}
diff --git a/src/main.tsx b/src/main.tsx
index bef5202..eff7ccc 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,10 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import "./index.css";
+import App from "./App.tsx";
-createRoot(document.getElementById('root')!).render(
+createRoot(document.getElementById("root")!).render(
,
-)
+);
diff --git a/src/pages/BLEConnectionsPage.tsx b/src/pages/BLEConnectionsPage.tsx
index de361b6..eb18a7d 100644
--- a/src/pages/BLEConnectionsPage.tsx
+++ b/src/pages/BLEConnectionsPage.tsx
@@ -31,8 +31,10 @@ export function BLEConnectionsPage() {
const [editingIndex, setEditingIndex] = useState(null);
const [editName, setEditName] = useState("");
- const [showOutputPriorityWarning, setShowOutputPriorityWarning] = useState(false);
- const [pendingOutputPriority, setPendingOutputPriority] = useState(null);
+ const [showOutputPriorityWarning, setShowOutputPriorityWarning] =
+ useState(false);
+ const [pendingOutputPriority, setPendingOutputPriority] =
+ useState(null);
const startEditing = (index: number, currentName: string) => {
setEditingIndex(index);
@@ -141,7 +143,11 @@ export function BLEConnectionsPage() {
? "bg-[var(--color-electric)]/20 border border-[var(--color-electric)]/40"
: "bg-[var(--color-surface)] border border-[var(--color-border)] hover:border-[var(--color-border-hover)]"
}`}
- onClick={() => handleOutputPriorityChange(OutputPriority.OUTPUT_PRIORITY_USB)}
+ onClick={() =>
+ handleOutputPriorityChange(
+ OutputPriority.OUTPUT_PRIORITY_USB,
+ )
+ }
disabled={isLoading}
>
handleOutputPriorityChange(OutputPriority.OUTPUT_PRIORITY_BLE)}
+ onClick={() =>
+ handleOutputPriorityChange(
+ OutputPriority.OUTPUT_PRIORITY_BLE,
+ )
+ }
disabled={isLoading}
>
-
+
Change Output Priority?
- Changing the output priority may disconnect DYA Studio from your keyboard.
+ Changing the output priority may disconnect DYA Studio from
+ your keyboard.
You will need to reconnect manually after the change.
diff --git a/src/pages/BatteryPage.tsx b/src/pages/BatteryPage.tsx
index 9ab2d04..b6b7a9d 100644
--- a/src/pages/BatteryPage.tsx
+++ b/src/pages/BatteryPage.tsx
@@ -22,9 +22,12 @@ export function BatteryPage() {
});
// Format last updated time
- const lastUpdated = currentLevels.length > 0
- ? new Date(Math.max(...currentLevels.map(d => d.timestamp)) * 1000).toLocaleTimeString()
- : "--:--";
+ const lastUpdated =
+ currentLevels.length > 0
+ ? new Date(
+ Math.max(...currentLevels.map((d) => d.timestamp)) * 1000,
+ ).toLocaleTimeString()
+ : "--:--";
// Device colors for chart
const deviceColors = [
@@ -57,7 +60,10 @@ export function BatteryPage() {
className="btn-ghost flex items-center gap-2"
aria-label="Refresh battery history"
>
-
+
Refresh
@@ -116,7 +122,7 @@ export function BatteryPage() {
Battery History
-
+
{isLoading && devices.length === 0 ? (
@@ -126,19 +132,24 @@ export function BatteryPage() {
) : devices.length === 0 ? (
- No battery history available. Connect keyboard to view battery history.
+ No battery history available. Connect keyboard to view battery
+ history.
) : (
-
+
)}
{/* Info Box */}
- Battery history is recorded on the keyboard and shows data from all connected devices.
- The timestamp resets when the keyboard restarts, indicated by dashed vertical lines in the chart.
+ Battery history is recorded on the keyboard and shows data from all
+ connected devices. The timestamp resets when the keyboard restarts,
+ indicated by dashed vertical lines in the chart.
diff --git a/src/pages/HealthCheckPage.tsx b/src/pages/HealthCheckPage.tsx
index 4a76c6e..503f0ed 100644
--- a/src/pages/HealthCheckPage.tsx
+++ b/src/pages/HealthCheckPage.tsx
@@ -103,8 +103,8 @@ export function HealthCheckPage() {
item.status === "ok"
? "text-[var(--color-neon)]"
: item.status === "error"
- ? "text-red-500"
- : "text-[var(--color-text-muted)]"
+ ? "text-red-500"
+ : "text-[var(--color-text-muted)]"
}`}
>
{item.status}
diff --git a/src/pages/__tests__/BLEConnectionsPage.test.tsx b/src/pages/__tests__/BLEConnectionsPage.test.tsx
index c9e47e4..3fdba97 100644
--- a/src/pages/__tests__/BLEConnectionsPage.test.tsx
+++ b/src/pages/__tests__/BLEConnectionsPage.test.tsx
@@ -175,14 +175,18 @@ describe("BLEConnectionsPage", () => {
it("should show refresh button when profiles exist", () => {
renderComponent({ isConnected: true }, { profiles: mockProfiles });
- const refreshButtons = screen.getAllByRole("button", { name: /refresh/i });
+ const refreshButtons = screen.getAllByRole("button", {
+ name: /refresh/i,
+ });
// Should have two refresh buttons: one for output priority and one for profiles
expect(refreshButtons.length).toBeGreaterThanOrEqual(1);
});
it("should not show refresh button emoji", () => {
renderComponent({ isConnected: true }, { profiles: mockProfiles });
- const refreshButtons = screen.getAllByRole("button", { name: /refresh/i });
+ const refreshButtons = screen.getAllByRole("button", {
+ name: /refresh/i,
+ });
// Check that none of the refresh buttons contain emoji
refreshButtons.forEach((button) => {
expect(button.textContent).not.toContain("🔄");
@@ -283,7 +287,9 @@ describe("BLEConnectionsPage", () => {
{ profiles: mockProfiles, loadProfiles: mockLoadProfiles },
);
- const refreshButton = screen.getByRole("button", { name: "Refresh profiles" });
+ const refreshButton = screen.getByRole("button", {
+ name: "Refresh profiles",
+ });
fireEvent.click(refreshButton);
await waitFor(() => {
@@ -395,12 +401,14 @@ describe("BLEConnectionsPage", () => {
const unpairButton = screen.getAllByRole("button", {
name: /unpair/i,
})[0];
- const refreshButtons = screen.getAllByRole("button", { name: /refresh/i });
+ const refreshButtons = screen.getAllByRole("button", {
+ name: /refresh/i,
+ });
expect(switchButton).toBeDisabled();
expect(unpairButton).toBeDisabled();
// All refresh buttons should be disabled when loading
- refreshButtons.forEach(button => {
+ refreshButtons.forEach((button) => {
expect(button).toBeDisabled();
});
});
diff --git a/src/pages/__tests__/BatteryPage.test.tsx b/src/pages/__tests__/BatteryPage.test.tsx
index df9806e..bf16220 100644
--- a/src/pages/__tests__/BatteryPage.test.tsx
+++ b/src/pages/__tests__/BatteryPage.test.tsx
@@ -8,7 +8,9 @@ import { useBatteryHistory } from "../../hooks/useBatteryHistory";
// Mock the useBatteryHistory hook
jest.mock("../../hooks/useBatteryHistory");
-const mockUseBatteryHistory = useBatteryHistory as jest.MockedFunction
;
+const mockUseBatteryHistory = useBatteryHistory as jest.MockedFunction<
+ typeof useBatteryHistory
+>;
describe("BatteryPage", () => {
beforeEach(() => {
@@ -27,7 +29,9 @@ describe("BatteryPage", () => {
render();
expect(screen.getByText("Battery Status")).toBeInTheDocument();
- expect(screen.getByText("Monitor battery levels and history")).toBeInTheDocument();
+ expect(
+ screen.getByText("Monitor battery levels and history"),
+ ).toBeInTheDocument();
});
it("should show placeholder when no devices are connected", () => {
@@ -106,7 +110,7 @@ describe("BatteryPage", () => {
// Check that device names appear (may appear multiple times - in cards and charts)
expect(screen.getAllByText("Central").length).toBeGreaterThan(0);
expect(screen.getAllByText("Peripheral 1").length).toBeGreaterThan(0);
-
+
// Check battery percentages
expect(screen.getByText("80%")).toBeInTheDocument();
expect(screen.getByText("70%")).toBeInTheDocument();
@@ -114,7 +118,7 @@ describe("BatteryPage", () => {
it("should have refresh button", () => {
const mockLoadBatteryHistory = jest.fn();
-
+
mockUseBatteryHistory.mockReturnValue({
devices: [],
isLoading: false,
@@ -141,10 +145,10 @@ describe("BatteryPage", () => {
render();
expect(
- screen.getByText(/Battery history is recorded on the keyboard/i)
+ screen.getByText(/Battery history is recorded on the keyboard/i),
).toBeInTheDocument();
expect(
- screen.getByText(/timestamp resets when the keyboard restarts/i)
+ screen.getByText(/timestamp resets when the keyboard restarts/i),
).toBeInTheDocument();
});
});
diff --git a/src/pages/__tests__/KeymapPage.test.tsx b/src/pages/__tests__/KeymapPage.test.tsx
index 1738e4b..5478dfe 100644
--- a/src/pages/__tests__/KeymapPage.test.tsx
+++ b/src/pages/__tests__/KeymapPage.test.tsx
@@ -99,9 +99,14 @@ describe("KeymapPage", () => {
setBinding: jest.fn().mockResolvedValue(true),
resetBinding: jest.fn().mockResolvedValue(true),
moveLayer: jest.fn().mockResolvedValue(true),
- addLayer: jest.fn().mockResolvedValue({ index: 0, layer: { id: 0, name: "New", bindings: [] } }),
+ addLayer: jest.fn().mockResolvedValue({
+ index: 0,
+ layer: { id: 0, name: "New", bindings: [] },
+ }),
removeLayer: jest.fn().mockResolvedValue(true),
- restoreLayer: jest.fn().mockResolvedValue({ id: 0, name: "Restored", bindings: [] }),
+ restoreLayer: jest
+ .fn()
+ .mockResolvedValue({ id: 0, name: "Restored", bindings: [] }),
availableLayers: 4,
removedLayerIds: [],
saveChanges: jest.fn().mockResolvedValue(true),
@@ -133,7 +138,7 @@ describe("KeymapPage", () => {
-
+ ,
);
};
@@ -142,14 +147,14 @@ describe("KeymapPage", () => {
renderComponent();
expect(screen.getByText("Keymap")).toBeInTheDocument();
expect(
- screen.getByText("Configure key bindings and layers")
+ screen.getByText("Configure key bindings and layers"),
).toBeInTheDocument();
});
it("should show connect message when not connected", () => {
renderComponent();
expect(
- screen.getByText("Connect your keyboard to edit keymaps")
+ screen.getByText("Connect your keyboard to edit keymaps"),
).toBeInTheDocument();
});
@@ -161,10 +166,7 @@ describe("KeymapPage", () => {
describe("Connected State - Loading", () => {
it("should show loading state", () => {
- renderComponent(
- { isConnected: true },
- { isLoading: true, keymap: null }
- );
+ renderComponent({ isConnected: true }, { isLoading: true, keymap: null });
expect(screen.getByText("Loading keymap data...")).toBeInTheDocument();
});
});
@@ -173,7 +175,11 @@ describe("KeymapPage", () => {
it("should show error message", () => {
renderComponent(
{ isConnected: true },
- { error: "Failed to load keymap", keymap: mockKeymap, physicalLayouts: mockPhysicalLayouts }
+ {
+ error: "Failed to load keymap",
+ keymap: mockKeymap,
+ physicalLayouts: mockPhysicalLayouts,
+ },
);
expect(screen.getByText("Failed to load keymap")).toBeInTheDocument();
});
@@ -187,7 +193,7 @@ describe("KeymapPage", () => {
keymap: mockKeymap,
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
- }
+ },
);
expect(screen.getByText("Base")).toBeInTheDocument();
@@ -202,7 +208,7 @@ describe("KeymapPage", () => {
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
hasUnsavedChanges: true,
- }
+ },
);
expect(screen.getByText("● Unsaved changes")).toBeInTheDocument();
@@ -215,7 +221,7 @@ describe("KeymapPage", () => {
keymap: mockKeymap,
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
- }
+ },
);
expect(screen.getByText("Save")).toBeInTheDocument();
@@ -230,7 +236,7 @@ describe("KeymapPage", () => {
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
hasUnsavedChanges: false,
- }
+ },
);
const saveButton = screen.getByText("Save").closest("button");
@@ -245,7 +251,7 @@ describe("KeymapPage", () => {
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
hasUnsavedChanges: true,
- }
+ },
);
const saveButton = screen.getByText("Save").closest("button");
@@ -262,7 +268,7 @@ describe("KeymapPage", () => {
keymap: mockKeymap,
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
- }
+ },
);
const lowerTab = screen.getByText("Lower");
@@ -270,7 +276,7 @@ describe("KeymapPage", () => {
// The Lower tab should now have the active styling
expect(lowerTab.closest("button")).toHaveClass(
- "bg-[var(--color-electric)]/20"
+ "bg-[var(--color-electric)]/20",
);
});
});
@@ -283,11 +289,15 @@ describe("KeymapPage", () => {
keymap: mockKeymap,
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
- }
+ },
);
- expect(screen.getByLabelText("Move layer up (higher priority)")).toBeInTheDocument();
- expect(screen.getByLabelText("Move layer down (lower priority)")).toBeInTheDocument();
+ expect(
+ screen.getByLabelText("Move layer up (higher priority)"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText("Move layer down (lower priority)"),
+ ).toBeInTheDocument();
});
it("should disable move up button for first layer", () => {
@@ -297,10 +307,12 @@ describe("KeymapPage", () => {
keymap: mockKeymap,
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
- }
+ },
);
- const moveUpButton = screen.getByLabelText("Move layer up (higher priority)");
+ const moveUpButton = screen.getByLabelText(
+ "Move layer up (higher priority)",
+ );
expect(moveUpButton).toBeDisabled();
});
@@ -314,10 +326,12 @@ describe("KeymapPage", () => {
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
moveLayer: mockMoveLayer,
- }
+ },
);
- const moveDownButton = screen.getByLabelText("Move layer down (lower priority)");
+ const moveDownButton = screen.getByLabelText(
+ "Move layer down (lower priority)",
+ );
await user.click(moveDownButton);
expect(mockMoveLayer).toHaveBeenCalledWith(0, 1);
@@ -330,11 +344,11 @@ describe("KeymapPage", () => {
{ isConnected: true },
{
unlockRequired: false,
- }
+ },
);
expect(
- screen.queryByText("Keyboard Unlock Required")
+ screen.queryByText("Keyboard Unlock Required"),
).not.toBeInTheDocument();
});
@@ -350,11 +364,11 @@ describe("KeymapPage", () => {
keymap: mockKeymap,
physicalLayouts: mockPhysicalLayouts,
behaviors: mockBehaviors,
- }
+ },
);
expect(
- screen.getByText(/Click on a key to modify its binding/)
+ screen.getByText(/Click on a key to modify its binding/),
).toBeInTheDocument();
});
@@ -362,8 +376,8 @@ describe("KeymapPage", () => {
renderComponent();
expect(
screen.getByText(
- "Connect your keyboard to edit keymaps. Click on a key to modify its binding."
- )
+ "Connect your keyboard to edit keymaps. Click on a key to modify its binding.",
+ ),
).toBeInTheDocument();
});
});
diff --git a/src/pages/__tests__/TrackballPage.test.tsx b/src/pages/__tests__/TrackballPage.test.tsx
index e237a08..00315f2 100644
--- a/src/pages/__tests__/TrackballPage.test.tsx
+++ b/src/pages/__tests__/TrackballPage.test.tsx
@@ -9,7 +9,10 @@ import { useRuntimeInputProcessor } from "../../hooks/useRuntimeInputProcessor";
// Mock the useRuntimeInputProcessor hook
jest.mock("../../hooks/useRuntimeInputProcessor");
-const mockUseRuntimeInputProcessor = useRuntimeInputProcessor as jest.MockedFunction;
+const mockUseRuntimeInputProcessor =
+ useRuntimeInputProcessor as jest.MockedFunction<
+ typeof useRuntimeInputProcessor
+ >;
describe("TrackballPage", () => {
beforeEach(() => {
@@ -29,7 +32,11 @@ describe("TrackballPage", () => {
render();
expect(screen.getByText("Trackball Settings")).toBeInTheDocument();
- expect(screen.getByText("Adjust sensitivity and behavior via runtime input processor")).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "Adjust sensitivity and behavior via runtime input processor",
+ ),
+ ).toBeInTheDocument();
});
it("should show loading state when initially loading", () => {
@@ -44,7 +51,9 @@ describe("TrackballPage", () => {
render();
- expect(screen.getByText("Loading trackball settings...")).toBeInTheDocument();
+ expect(
+ screen.getByText("Loading trackball settings..."),
+ ).toBeInTheDocument();
});
it("should display error message", () => {
@@ -75,7 +84,9 @@ describe("TrackballPage", () => {
render();
- expect(screen.getByText(/No runtime input processor found/)).toBeInTheDocument();
+ expect(
+ screen.getByText(/No runtime input processor found/),
+ ).toBeInTheDocument();
});
it("should display processor information when loaded", () => {
@@ -177,7 +188,9 @@ describe("TrackballPage", () => {
// Click on 2.0x speed button in the ButtonListSelector
const speedButtons = screen.getAllByText("2.0x");
// The button is the one in the ButtonListSelector (not the display value)
- const speedButton = speedButtons.find(el => el.tagName === 'SPAN' && el.parentElement?.tagName === 'BUTTON');
+ const speedButton = speedButtons.find(
+ (el) => el.tagName === "SPAN" && el.parentElement?.tagName === "BUTTON",
+ );
if (speedButton && speedButton.parentElement) {
await user.click(speedButton.parentElement);
}
@@ -188,7 +201,7 @@ describe("TrackballPage", () => {
});
expect(mockSetScaling).toHaveBeenCalledWith("trackpad", 200, 100);
-
+
jest.useRealTimers();
});
@@ -218,7 +231,9 @@ describe("TrackballPage", () => {
// Click on 90° rotation button in the ButtonListSelector
const rotationButtons = screen.getAllByText("90°");
// The button is the one in the ButtonListSelector (not the display value)
- const rotationButton = rotationButtons.find(el => el.tagName === 'SPAN' && el.parentElement?.tagName === 'BUTTON');
+ const rotationButton = rotationButtons.find(
+ (el) => el.tagName === "SPAN" && el.parentElement?.tagName === "BUTTON",
+ );
if (rotationButton && rotationButton.parentElement) {
await user.click(rotationButton.parentElement);
}
@@ -229,7 +244,7 @@ describe("TrackballPage", () => {
});
expect(mockSetRotation).toHaveBeenCalledWith("trackpad", 90);
-
+
jest.useRealTimers();
});
@@ -278,6 +293,8 @@ describe("TrackballPage", () => {
render();
- expect(screen.getByText(/Runtime input processor allows you to adjust/)).toBeInTheDocument();
+ expect(
+ screen.getByText(/Runtime input processor allows you to adjust/),
+ ).toBeInTheDocument();
});
});