Merge pull request #249 from EtienneLescot/feat/webcam-selector-optimization
feat: added webcam source selector and optimized horizontal UI
This commit is contained in:
+6
-6
@@ -21,8 +21,8 @@ export function createHudOverlayWindow(): BrowserWindow {
|
||||
const primaryDisplay = screen.getPrimaryDisplay();
|
||||
const { workArea } = primaryDisplay;
|
||||
|
||||
const windowWidth = 500;
|
||||
const windowHeight = 155;
|
||||
const windowWidth = 600;
|
||||
const windowHeight = 160;
|
||||
|
||||
const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2);
|
||||
const y = Math.floor(workArea.y + workArea.height - windowHeight - 5);
|
||||
@@ -30,10 +30,10 @@ export function createHudOverlayWindow(): BrowserWindow {
|
||||
const win = new BrowserWindow({
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
minWidth: 500,
|
||||
maxWidth: 500,
|
||||
minHeight: 155,
|
||||
maxHeight: 155,
|
||||
minWidth: 600,
|
||||
maxWidth: 600,
|
||||
minHeight: 160,
|
||||
maxHeight: 160,
|
||||
x: x,
|
||||
y: y,
|
||||
frame: false,
|
||||
|
||||
Generated
+762
@@ -52,6 +52,9 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
@@ -64,6 +67,7 @@
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"fast-check": "^4.5.2",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^29.0.1",
|
||||
"lint-staged": "^16.3.2",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.18",
|
||||
@@ -79,6 +83,13 @@
|
||||
"npm": "10.9.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
|
||||
"integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
@@ -91,6 +102,67 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz",
|
||||
"integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^3.1.1",
|
||||
"@csstools/css-color-parser": "^4.0.2",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0",
|
||||
"lru-cache": "^11.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
||||
"version": "11.2.7",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz",
|
||||
"integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.2.1",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.2.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
|
||||
"version": "11.2.7",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@@ -565,6 +637,159 @@
|
||||
"node": ">=14.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-tree": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||
"integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz",
|
||||
"integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz",
|
||||
"integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^6.0.2",
|
||||
"@csstools/css-calc": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz",
|
||||
"integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"peerDependencies": {
|
||||
"css-tree": "^3.2.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"css-tree": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@develar/schema-utils": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
||||
@@ -1825,6 +2050,24 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@exodus/bytes": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
|
||||
"integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@noble/hashes": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@fix-webm-duration/fix": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@fix-webm-duration/fix/-/fix-1.0.1.tgz",
|
||||
@@ -4244,6 +4487,96 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^5.0.1",
|
||||
"aria-query": "5.3.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.5.0",
|
||||
"picocolors": "1.1.1",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
|
||||
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": "^4.4.0",
|
||||
"aria-query": "^5.0.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"dom-accessibility-api": "^0.6.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
|
||||
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/react": {
|
||||
"version": "16.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
|
||||
"integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@types/react": "^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/user-event": {
|
||||
"version": "14.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
|
||||
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@testing-library/dom": ">=7.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tokenizer/token": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||
@@ -4261,6 +4594,14 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -5342,6 +5683,16 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
@@ -5529,6 +5880,16 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -6389,6 +6750,27 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.27.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -6421,6 +6803,20 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -6449,6 +6845,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
@@ -6579,6 +6982,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -6756,6 +7169,14 @@
|
||||
"resize-observer-polyfill": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-accessibility-api": {
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
@@ -7185,6 +7606,19 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
@@ -8276,6 +8710,19 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
@@ -8671,6 +9118,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
@@ -8842,6 +9296,70 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "29.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz",
|
||||
"integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^5.0.1",
|
||||
"@asamuzakjp/dom-selector": "^7.0.3",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.1",
|
||||
"@exodus/bytes": "^1.15.0",
|
||||
"css-tree": "^3.2.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^6.0.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.2.7",
|
||||
"parse5": "^8.0.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.1",
|
||||
"undici": "^7.24.5",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.1",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/lru-cache": {
|
||||
"version": "11.2.7",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/tough-cookie": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
|
||||
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -9549,6 +10067,17 @@
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lzma-native": {
|
||||
"version": "8.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz",
|
||||
@@ -9666,6 +10195,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.27.1",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/mediabunny": {
|
||||
"version": "1.25.1",
|
||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.25.1.tgz",
|
||||
@@ -9790,6 +10326,16 @@
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
@@ -10567,6 +11113,19 @@
|
||||
"integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
||||
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
@@ -11174,6 +11733,44 @@
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/proc-log": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
|
||||
@@ -11724,6 +12321,20 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"indent-string": "^4.0.0",
|
||||
"strip-indent": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
@@ -11820,6 +12431,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
|
||||
@@ -12085,6 +12706,19 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
@@ -12603,6 +13237,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"min-indent": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strtok3": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
|
||||
@@ -12868,6 +13515,13 @@
|
||||
"camelcase": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
|
||||
@@ -13217,6 +13871,26 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.27",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz",
|
||||
"integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.0.27"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.0.27",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz",
|
||||
"integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
@@ -13281,6 +13955,19 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
@@ -13344,6 +14031,16 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz",
|
||||
"integrity": "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
@@ -14281,6 +14978,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/wcwidth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
@@ -14300,6 +15010,41 @@
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.11.0",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -14405,6 +15150,16 @@
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-parse-from-string": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
|
||||
@@ -14446,6 +15201,13 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -70,6 +70,9 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
@@ -82,6 +85,7 @@
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"fast-check": "^4.5.2",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^29.0.1",
|
||||
"lint-staged": "^16.3.2",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.18",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { type Locale, SUPPORTED_LOCALES } from "@/i18n/config";
|
||||
import { getLocaleName } from "@/i18n/loader";
|
||||
import { isMac as getIsMac } from "@/utils/platformUtils";
|
||||
import { useAudioLevelMeter } from "../../hooks/useAudioLevelMeter";
|
||||
import { useCameraDevices } from "../../hooks/useCameraDevices";
|
||||
import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices";
|
||||
import { useScreenRecorder } from "../../hooks/useScreenRecorder";
|
||||
import { requestCameraAccess } from "../../lib/requestCameraAccess";
|
||||
@@ -86,23 +87,64 @@ export function LaunchWindow() {
|
||||
setSystemAudioEnabled,
|
||||
webcamEnabled,
|
||||
setWebcamEnabled,
|
||||
webcamDeviceId,
|
||||
setWebcamDeviceId,
|
||||
} = useScreenRecorder();
|
||||
const [recordingStart, setRecordingStart] = useState<number | null>(null);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
|
||||
const showMicControls = microphoneEnabled && !recording;
|
||||
const { devices, selectedDeviceId, setSelectedDeviceId } =
|
||||
useMicrophoneDevices(microphoneEnabled);
|
||||
const showWebcamControls = webcamEnabled && !recording;
|
||||
|
||||
const [isMicHovered, setIsMicHovered] = useState(false);
|
||||
const [isMicFocused, setIsMicFocused] = useState(false);
|
||||
const micExpanded = isMicHovered || isMicFocused;
|
||||
|
||||
const [isWebcamHovered, setIsWebcamHovered] = useState(false);
|
||||
const [isWebcamFocused, setIsWebcamFocused] = useState(false);
|
||||
const webcamExpanded = isWebcamHovered || isWebcamFocused;
|
||||
|
||||
const {
|
||||
devices: micDevices,
|
||||
selectedDeviceId: selectedMicId,
|
||||
setSelectedDeviceId: setSelectedMicId,
|
||||
} = useMicrophoneDevices(microphoneEnabled);
|
||||
const {
|
||||
devices: cameraDevices,
|
||||
selectedDeviceId: selectedCameraId,
|
||||
setSelectedDeviceId: setSelectedCameraId,
|
||||
isLoading: isCameraDevicesLoading,
|
||||
error: cameraDevicesError,
|
||||
} = useCameraDevices(webcamEnabled);
|
||||
|
||||
const selectedMicLabel =
|
||||
micDevices.find((d) => d.deviceId === (microphoneDeviceId || selectedMicId))?.label ||
|
||||
t("audio.defaultMicrophone");
|
||||
const selectedCameraLabel = isCameraDevicesLoading
|
||||
? t("webcam.searching")
|
||||
: cameraDevicesError
|
||||
? t("webcam.unavailable")
|
||||
: cameraDevices.length === 0
|
||||
? t("webcam.noneFound")
|
||||
: cameraDevices.find((d) => d.deviceId === (webcamDeviceId || selectedCameraId))?.label ||
|
||||
t("webcam.defaultCamera");
|
||||
|
||||
const { level } = useAudioLevelMeter({
|
||||
enabled: showMicControls,
|
||||
deviceId: microphoneDeviceId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDeviceId && selectedDeviceId !== "default") {
|
||||
setMicrophoneDeviceId(selectedDeviceId);
|
||||
if (selectedMicId && selectedMicId !== "default") {
|
||||
setMicrophoneDeviceId(selectedMicId);
|
||||
}
|
||||
}, [selectedDeviceId, setMicrophoneDeviceId]);
|
||||
}, [selectedMicId, setMicrophoneDeviceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCameraId) {
|
||||
setWebcamDeviceId(selectedCameraId);
|
||||
}
|
||||
}, [selectedCameraId, setWebcamDeviceId]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
@@ -199,10 +241,10 @@ export function LaunchWindow() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-end justify-center bg-transparent relative">
|
||||
<div className={`w-screen h-screen bg-transparent ${styles.electronDrag}`}>
|
||||
{/* Language switcher — top-left, beside traffic lights */}
|
||||
<div
|
||||
className={`absolute top-2 flex items-center gap-1 px-2 py-1 rounded-md text-white/50 hover:text-white/90 hover:bg-white/10 transition-all duration-150 ${isMac ? "left-[72px]" : "left-2"} ${styles.electronNoDrag}`}
|
||||
className={`fixed top-2 flex items-center gap-1 px-2 py-1 rounded-md text-white/50 hover:text-white/90 hover:bg-white/10 transition-all duration-150 ${isMac ? "left-[72px]" : "left-2"} ${styles.electronNoDrag}`}
|
||||
>
|
||||
<Languages size={14} />
|
||||
<select
|
||||
@@ -219,165 +261,260 @@ export function LaunchWindow() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={`flex flex-col items-center gap-2 mx-auto ${styles.electronDrag}`}>
|
||||
{/* Mic controls panel */}
|
||||
{showMicControls && (
|
||||
<div
|
||||
className={`flex items-center gap-2 px-4 py-2 bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[16px] backdrop-saturate-[140%] border border-[rgba(80,80,120,0.25)] rounded-2xl shadow-mic-panel animate-mic-panel-in ${styles.electronNoDrag}`}
|
||||
>
|
||||
<div className="relative flex-1" style={{ maxWidth: "70%" }}>
|
||||
<select
|
||||
value={microphoneDeviceId || selectedDeviceId}
|
||||
onChange={(e) => {
|
||||
setSelectedDeviceId(e.target.value);
|
||||
setMicrophoneDeviceId(e.target.value);
|
||||
}}
|
||||
className="w-full appearance-none bg-white/10 text-white text-xs rounded-full pl-3 pr-7 py-2 border border-white/20 outline-none truncate"
|
||||
>
|
||||
{devices.map((device) => (
|
||||
<option key={device.deviceId} value={device.deviceId}>
|
||||
{device.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-white/60 pointer-events-none"
|
||||
{/* Device selectors — fixed above HUD bar, viewport-relative, never clipped */}
|
||||
{(showMicControls || showWebcamControls) && (
|
||||
<div
|
||||
className={`fixed bottom-[60px] left-1/2 -translate-x-1/2 flex items-center gap-2 animate-mic-panel-in ${styles.electronNoDrag}`}
|
||||
>
|
||||
{/* Mic selector */}
|
||||
{showMicControls && (
|
||||
<div
|
||||
className={`flex items-center gap-2 px-3 py-1.5 h-[36px] bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[24px] border border-white/10 rounded-xl shadow-2xl transition-all duration-300 overflow-hidden ${!micExpanded ? "opacity-60 grayscale-[0.5]" : "opacity-100"}`}
|
||||
onMouseEnter={() => setIsMicHovered(true)}
|
||||
onMouseLeave={() => setIsMicHovered(false)}
|
||||
onFocus={() => setIsMicFocused(true)}
|
||||
onBlur={() => setIsMicFocused(false)}
|
||||
style={{ width: micExpanded ? "240px" : "140px", transition: "width 300ms ease" }}
|
||||
>
|
||||
<div className="relative flex-1 min-w-0">
|
||||
{!micExpanded && (
|
||||
<div className="text-white/60 text-[10px] font-medium truncate">
|
||||
{selectedMicLabel}
|
||||
</div>
|
||||
)}
|
||||
<select
|
||||
value={microphoneDeviceId || selectedMicId}
|
||||
onChange={(e) => {
|
||||
setSelectedMicId(e.target.value);
|
||||
setMicrophoneDeviceId(e.target.value);
|
||||
}}
|
||||
className={`w-full appearance-none bg-white/5 text-white text-[11px] rounded-lg pl-2 pr-6 py-1 border border-white/10 outline-none hover:bg-white/10 transition-colors cursor-pointer ${!micExpanded ? "sr-only" : ""}`}
|
||||
>
|
||||
{micDevices.map((device) => (
|
||||
<option key={device.deviceId} value={device.deviceId} className="bg-[#1c1c24]">
|
||||
{device.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{micExpanded && (
|
||||
<ChevronDown
|
||||
size={12}
|
||||
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-white/40 pointer-events-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<AudioLevelMeter
|
||||
level={level}
|
||||
className={`${micExpanded ? "w-16" : "w-8"} h-2 transition-all duration-300`}
|
||||
/>
|
||||
</div>
|
||||
<AudioLevelMeter level={level} className="w-24 h-4" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main pill bar */}
|
||||
<div className="flex items-center gap-1.5 px-2 py-1.5 isolate rounded-full shadow-hud-bar bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[16px] backdrop-saturate-[140%] border border-[rgba(80,80,120,0.25)]">
|
||||
{/* Drag handle */}
|
||||
<div className={`flex items-center px-1 ${styles.electronDrag}`}>
|
||||
{getIcon("drag", "text-white/30")}
|
||||
</div>
|
||||
|
||||
{/* Source selector */}
|
||||
<button
|
||||
className={`${hudGroupClasses} p-2 ${styles.electronNoDrag}`}
|
||||
onClick={openSourceSelector}
|
||||
disabled={recording}
|
||||
title={selectedSource}
|
||||
>
|
||||
{getIcon("monitor", "text-white/80")}
|
||||
<span className="text-white/70 text-[11px] max-w-[72px] truncate">
|
||||
{selectedSource}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Audio controls group */}
|
||||
<div className={`${hudGroupClasses} ${styles.electronNoDrag}`}>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
|
||||
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
|
||||
disabled={recording}
|
||||
title={
|
||||
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
|
||||
}
|
||||
>
|
||||
{systemAudioEnabled
|
||||
? getIcon("volumeOn", "text-green-400")
|
||||
: getIcon("volumeOff", "text-white/40")}
|
||||
</button>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${microphoneEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
|
||||
onClick={toggleMicrophone}
|
||||
disabled={recording}
|
||||
title={microphoneEnabled ? t("audio.disableMicrophone") : t("audio.enableMicrophone")}
|
||||
>
|
||||
{microphoneEnabled
|
||||
? getIcon("micOn", "text-green-400")
|
||||
: getIcon("micOff", "text-white/40")}
|
||||
</button>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
|
||||
onClick={async () => {
|
||||
await setWebcamEnabled(!webcamEnabled);
|
||||
}}
|
||||
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
|
||||
>
|
||||
{webcamEnabled
|
||||
? getIcon("webcamOn", "text-green-400")
|
||||
: getIcon("webcamOff", "text-white/40")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Record/Stop group */}
|
||||
<button
|
||||
className={`flex items-center gap-0.5 rounded-full p-2 transition-colors duration-150 ${styles.electronNoDrag} ${
|
||||
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
|
||||
}`}
|
||||
onClick={hasSelectedSource ? toggleRecording : openSourceSelector}
|
||||
disabled={!hasSelectedSource && !recording}
|
||||
style={{ flex: "0 0 auto" }}
|
||||
>
|
||||
{recording ? (
|
||||
<>
|
||||
{getIcon("stop", "text-red-400")}
|
||||
<span className="text-red-400 text-xs font-semibold tabular-nums">
|
||||
{formatTimePadded(elapsed)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
getIcon("record", hasSelectedSource ? "text-white/80" : "text-white/30")
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Restart recording */}
|
||||
{recording && (
|
||||
<Tooltip content={t("tooltips.restartRecording")}>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
|
||||
onClick={restartRecording}
|
||||
>
|
||||
{getIcon("restart", "text-white/60")}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Open video file */}
|
||||
<Tooltip content={t("tooltips.openVideoFile")}>
|
||||
{/* Webcam selector */}
|
||||
{showWebcamControls && (
|
||||
<div
|
||||
className={`flex items-center gap-2 px-3 py-1.5 h-[36px] bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[24px] border border-white/10 rounded-xl shadow-2xl transition-all duration-300 overflow-hidden ${!webcamExpanded ? "opacity-60 grayscale-[0.5]" : "opacity-100"}`}
|
||||
onMouseEnter={() => setIsWebcamHovered(true)}
|
||||
onMouseLeave={() => setIsWebcamHovered(false)}
|
||||
onFocus={() => setIsWebcamFocused(true)}
|
||||
onBlur={() => setIsWebcamFocused(false)}
|
||||
style={{ width: webcamExpanded ? "240px" : "140px", transition: "width 300ms ease" }}
|
||||
>
|
||||
<div className="relative flex-1 min-w-0">
|
||||
{!webcamExpanded && (
|
||||
<div className="text-white/60 text-[10px] font-medium truncate">
|
||||
{selectedCameraLabel}
|
||||
</div>
|
||||
)}
|
||||
{webcamExpanded &&
|
||||
(isCameraDevicesLoading ? (
|
||||
<span className="text-white/40 text-[10px] italic">
|
||||
{t("webcam.searching")}
|
||||
</span>
|
||||
) : cameraDevicesError ? (
|
||||
<span className="text-white/40 text-[10px] italic">
|
||||
{t("webcam.unavailable")}
|
||||
</span>
|
||||
) : cameraDevices.length === 0 ? (
|
||||
<span className="text-white/40 text-[10px] italic">
|
||||
{t("webcam.noneFound")}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<select
|
||||
value={webcamDeviceId || selectedCameraId}
|
||||
onChange={(e) => {
|
||||
setSelectedCameraId(e.target.value);
|
||||
setWebcamDeviceId(e.target.value);
|
||||
}}
|
||||
className="w-full appearance-none bg-white/5 text-white text-[11px] rounded-lg pl-2 pr-6 py-1 border border-white/10 outline-none hover:bg-white/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{cameraDevices.map((device) => (
|
||||
<option
|
||||
key={device.deviceId}
|
||||
value={device.deviceId}
|
||||
className="bg-[#1c1c24]"
|
||||
>
|
||||
{device.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown
|
||||
size={12}
|
||||
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-white/40 pointer-events-none"
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
{(!webcamExpanded || cameraDevices.length === 0) && (
|
||||
<select
|
||||
value={webcamDeviceId || selectedCameraId}
|
||||
onChange={(e) => {
|
||||
setSelectedCameraId(e.target.value);
|
||||
setWebcamDeviceId(e.target.value);
|
||||
}}
|
||||
className="sr-only"
|
||||
>
|
||||
{cameraDevices.map((device) => (
|
||||
<option key={device.deviceId} value={device.deviceId}>
|
||||
{device.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* HUD bar — fixed at bottom center, viewport-relative, never moves */}
|
||||
<div
|
||||
className={`fixed bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-1.5 px-2 py-1.5 rounded-full shadow-hud-bar bg-gradient-to-br from-[rgba(28,28,36,0.97)] to-[rgba(18,18,26,0.96)] backdrop-blur-[16px] backdrop-saturate-[140%] border border-[rgba(80,80,120,0.25)]`}
|
||||
>
|
||||
{/* Drag handle */}
|
||||
<div className={`flex items-center px-1 ${styles.electronDrag}`}>
|
||||
{getIcon("drag", "text-white/30")}
|
||||
</div>
|
||||
|
||||
{/* Source selector */}
|
||||
<button
|
||||
className={`${hudGroupClasses} p-2 ${styles.electronNoDrag}`}
|
||||
onClick={openSourceSelector}
|
||||
disabled={recording}
|
||||
title={selectedSource}
|
||||
>
|
||||
{getIcon("monitor", "text-white/80")}
|
||||
<span className="text-white/70 text-[11px] max-w-[72px] truncate">{selectedSource}</span>
|
||||
</button>
|
||||
|
||||
{/* Audio controls group */}
|
||||
<div className={`${hudGroupClasses} ${styles.electronNoDrag}`}>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
|
||||
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
|
||||
disabled={recording}
|
||||
title={
|
||||
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
|
||||
}
|
||||
>
|
||||
{systemAudioEnabled
|
||||
? getIcon("volumeOn", "text-green-400")
|
||||
: getIcon("volumeOff", "text-white/40")}
|
||||
</button>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${microphoneEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
|
||||
onClick={toggleMicrophone}
|
||||
disabled={recording}
|
||||
title={microphoneEnabled ? t("audio.disableMicrophone") : t("audio.enableMicrophone")}
|
||||
>
|
||||
{microphoneEnabled
|
||||
? getIcon("micOn", "text-green-400")
|
||||
: getIcon("micOff", "text-white/40")}
|
||||
</button>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
|
||||
onClick={async () => {
|
||||
await setWebcamEnabled(!webcamEnabled);
|
||||
}}
|
||||
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
|
||||
>
|
||||
{webcamEnabled
|
||||
? getIcon("webcamOn", "text-green-400")
|
||||
: getIcon("webcamOff", "text-white/40")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Record/Stop group */}
|
||||
<button
|
||||
className={`flex items-center gap-0.5 rounded-full p-2 transition-colors duration-150 ${styles.electronNoDrag} ${
|
||||
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
|
||||
}`}
|
||||
onClick={toggleRecording}
|
||||
disabled={!hasSelectedSource && !recording}
|
||||
style={{ flex: "0 0 auto" }}
|
||||
>
|
||||
{recording ? (
|
||||
<>
|
||||
{getIcon("stop", "text-red-400")}
|
||||
<span className="text-red-400 text-xs font-semibold tabular-nums">
|
||||
{formatTimePadded(elapsed)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
getIcon("record", hasSelectedSource ? "text-white/80" : "text-white/30")
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Restart recording */}
|
||||
{recording && (
|
||||
<Tooltip content={t("tooltips.restartRecording")}>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
|
||||
onClick={openVideoFile}
|
||||
disabled={recording}
|
||||
onClick={restartRecording}
|
||||
>
|
||||
{getIcon("videoFile", "text-white/60")}
|
||||
{getIcon("restart", "text-white/60")}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Open project */}
|
||||
<Tooltip content={t("tooltips.openProject")}>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
|
||||
onClick={openProjectFile}
|
||||
disabled={recording}
|
||||
>
|
||||
{getIcon("folder", "text-white/60")}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/* Open video file */}
|
||||
<Tooltip content={t("tooltips.openVideoFile")}>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
|
||||
onClick={openVideoFile}
|
||||
disabled={recording}
|
||||
>
|
||||
{getIcon("videoFile", "text-white/60")}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{/* Window controls */}
|
||||
<div className={`flex items-center gap-0.5 ${styles.electronNoDrag}`}>
|
||||
<button
|
||||
className={windowBtnClasses}
|
||||
title={t("tooltips.hideHUD")}
|
||||
onClick={sendHudOverlayHide}
|
||||
>
|
||||
{getIcon("minimize", "text-white")}
|
||||
</button>
|
||||
<button
|
||||
className={windowBtnClasses}
|
||||
title={t("tooltips.closeApp")}
|
||||
onClick={sendHudOverlayClose}
|
||||
>
|
||||
{getIcon("close", "text-white")}
|
||||
</button>
|
||||
</div>
|
||||
{/* Open project */}
|
||||
<Tooltip content={t("tooltips.openProject")}>
|
||||
<button
|
||||
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
|
||||
onClick={openProjectFile}
|
||||
disabled={recording}
|
||||
>
|
||||
{getIcon("folder", "text-white/60")}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{/* Window controls */}
|
||||
<div className={`flex items-center gap-0.5 ${styles.electronNoDrag}`}>
|
||||
<button
|
||||
className={windowBtnClasses}
|
||||
title={t("tooltips.hideHUD")}
|
||||
onClick={sendHudOverlayHide}
|
||||
>
|
||||
{getIcon("minimize", "text-white")}
|
||||
</button>
|
||||
<button
|
||||
className={windowBtnClasses}
|
||||
title={t("tooltips.closeApp")}
|
||||
onClick={sendHudOverlayClose}
|
||||
>
|
||||
{getIcon("close", "text-white")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useCameraDevices } from "./useCameraDevices";
|
||||
|
||||
// Mock navigator.mediaDevices
|
||||
const mockDevices = [
|
||||
{ kind: "videoinput", deviceId: "cam1", label: "Camera 1", groupId: "group1" },
|
||||
{ kind: "videoinput", deviceId: "cam2", label: "Camera 2", groupId: "group1" },
|
||||
{ kind: "audioinput", deviceId: "mic1", label: "Mic 1", groupId: "group2" },
|
||||
];
|
||||
|
||||
const mockGetUserMedia = vi.fn().mockResolvedValue({
|
||||
getTracks: () => [{ stop: vi.fn() }],
|
||||
});
|
||||
|
||||
const mockEnumerateDevices = vi.fn().mockResolvedValue(mockDevices);
|
||||
|
||||
Object.defineProperty(global.navigator, "mediaDevices", {
|
||||
value: {
|
||||
enumerateDevices: mockEnumerateDevices,
|
||||
getUserMedia: mockGetUserMedia,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
describe("useCameraDevices", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockEnumerateDevices.mockResolvedValue(mockDevices);
|
||||
mockGetUserMedia.mockResolvedValue({
|
||||
getTracks: () => [{ stop: vi.fn() }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should list video input devices", async () => {
|
||||
const { result } = renderHook(() => useCameraDevices(true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.devices).toHaveLength(2);
|
||||
});
|
||||
|
||||
expect(result.current.devices[0].label).toBe("Camera 1");
|
||||
expect(result.current.devices[1].deviceId).toBe("cam2");
|
||||
});
|
||||
|
||||
it("should set first device as default", async () => {
|
||||
const { result } = renderHook(() => useCameraDevices(true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedDeviceId).toBe("cam1");
|
||||
});
|
||||
});
|
||||
|
||||
it("should use device ID as fallback label when label is missing", async () => {
|
||||
mockEnumerateDevices.mockResolvedValueOnce([
|
||||
{ kind: "videoinput", deviceId: "cam1abc123456", label: "", groupId: "group1" },
|
||||
]);
|
||||
|
||||
const { result } = renderHook(() => useCameraDevices(true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.devices[0]?.label).toBe("Camera cam1abc1");
|
||||
});
|
||||
|
||||
expect(mockGetUserMedia).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should set error state when enumeration fails", async () => {
|
||||
mockEnumerateDevices.mockRejectedValueOnce(new Error("Permission denied"));
|
||||
|
||||
const { result } = renderHook(() => useCameraDevices(true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.error).toBe("Permission denied");
|
||||
});
|
||||
|
||||
expect(result.current.devices).toHaveLength(0);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("should fall back to first available device when selected device is unplugged", async () => {
|
||||
const { result } = renderHook(() => useCameraDevices(true));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedDeviceId).toBe("cam1");
|
||||
});
|
||||
|
||||
// Simulate cam1 being unplugged — only cam2 remains
|
||||
const cam2Only = [
|
||||
{ kind: "videoinput", deviceId: "cam2", label: "Camera 2", groupId: "group1" },
|
||||
];
|
||||
mockEnumerateDevices.mockResolvedValueOnce(cam2Only);
|
||||
|
||||
// Trigger devicechange event via the registered handler
|
||||
const devicechangeHandler = (
|
||||
navigator.mediaDevices.addEventListener as ReturnType<typeof vi.fn>
|
||||
).mock.calls[0]?.[1] as (() => void) | undefined;
|
||||
|
||||
await act(async () => {
|
||||
devicechangeHandler?.();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedDeviceId).toBe("cam2");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface CameraDevice {
|
||||
deviceId: string;
|
||||
label: string;
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
export function useCameraDevices(enabled: boolean = false) {
|
||||
const [devices, setDevices] = useState<CameraDevice[]>([]);
|
||||
const [selectedDeviceId, setSelectedDeviceId] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const selectedDeviceIdRef = useRef(selectedDeviceId);
|
||||
selectedDeviceIdRef.current = selectedDeviceId;
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
let mounted = true;
|
||||
|
||||
const loadDevices = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Enumerate without requesting a second stream — the recorder handles
|
||||
// the real acquisition; unlabeled devices fall back to their device ID.
|
||||
const allDevices = await navigator.mediaDevices.enumerateDevices();
|
||||
const videoInputs = allDevices
|
||||
.filter((device) => device.kind === "videoinput")
|
||||
.map((device) => ({
|
||||
deviceId: device.deviceId,
|
||||
label: device.label || `Camera ${device.deviceId.slice(0, 8)}`,
|
||||
groupId: device.groupId,
|
||||
}));
|
||||
|
||||
if (mounted) {
|
||||
setDevices(videoInputs);
|
||||
const currentId = selectedDeviceIdRef.current;
|
||||
const stillAvailable = videoInputs.some((d) => d.deviceId === currentId);
|
||||
if (!currentId || !stillAvailable) {
|
||||
setSelectedDeviceId(videoInputs[0]?.deviceId ?? "");
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load cameras");
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadDevices();
|
||||
|
||||
navigator.mediaDevices.addEventListener("devicechange", loadDevices);
|
||||
return () => {
|
||||
mounted = false;
|
||||
navigator.mediaDevices.removeEventListener("devicechange", loadDevices);
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
return { devices, selectedDeviceId, setSelectedDeviceId, isLoading, error };
|
||||
}
|
||||
@@ -47,6 +47,8 @@ type UseScreenRecorderReturn = {
|
||||
setMicrophoneEnabled: (enabled: boolean) => void;
|
||||
microphoneDeviceId: string | undefined;
|
||||
setMicrophoneDeviceId: (deviceId: string | undefined) => void;
|
||||
webcamDeviceId: string | undefined;
|
||||
setWebcamDeviceId: (deviceId: string | undefined) => void;
|
||||
systemAudioEnabled: boolean;
|
||||
setSystemAudioEnabled: (enabled: boolean) => void;
|
||||
webcamEnabled: boolean;
|
||||
@@ -85,6 +87,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
const [recording, setRecording] = useState(false);
|
||||
const [microphoneEnabled, setMicrophoneEnabled] = useState(false);
|
||||
const [microphoneDeviceId, setMicrophoneDeviceId] = useState<string | undefined>(undefined);
|
||||
const [webcamDeviceId, setWebcamDeviceId] = useState<string | undefined>(undefined);
|
||||
const [systemAudioEnabled, setSystemAudioEnabled] = useState(false);
|
||||
const [webcamEnabled, setWebcamEnabledState] = useState(false);
|
||||
const screenRecorder = useRef<RecorderHandle | null>(null);
|
||||
@@ -409,11 +412,18 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
try {
|
||||
webcamStream.current = await navigator.mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: {
|
||||
width: { ideal: WEBCAM_TARGET_WIDTH },
|
||||
height: { ideal: WEBCAM_TARGET_HEIGHT },
|
||||
frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE },
|
||||
},
|
||||
video: webcamDeviceId
|
||||
? {
|
||||
deviceId: { exact: webcamDeviceId },
|
||||
width: { ideal: WEBCAM_TARGET_WIDTH },
|
||||
height: { ideal: WEBCAM_TARGET_HEIGHT },
|
||||
frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE },
|
||||
}
|
||||
: {
|
||||
width: { ideal: WEBCAM_TARGET_WIDTH },
|
||||
height: { ideal: WEBCAM_TARGET_HEIGHT },
|
||||
frameRate: { ideal: WEBCAM_TARGET_FRAME_RATE, max: WEBCAM_TARGET_FRAME_RATE },
|
||||
},
|
||||
});
|
||||
} catch (cameraError) {
|
||||
console.warn("Failed to get webcam access:", cameraError);
|
||||
@@ -598,6 +608,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
setMicrophoneEnabled,
|
||||
microphoneDeviceId,
|
||||
setMicrophoneDeviceId,
|
||||
webcamDeviceId,
|
||||
setWebcamDeviceId,
|
||||
systemAudioEnabled,
|
||||
setSystemAudioEnabled,
|
||||
webcamEnabled,
|
||||
|
||||
@@ -10,11 +10,16 @@
|
||||
"enableSystemAudio": "Enable system audio",
|
||||
"disableSystemAudio": "Disable system audio",
|
||||
"enableMicrophone": "Enable microphone",
|
||||
"disableMicrophone": "Disable microphone"
|
||||
"disableMicrophone": "Disable microphone",
|
||||
"defaultMicrophone": "Default Microphone"
|
||||
},
|
||||
"webcam": {
|
||||
"enableWebcam": "Enable webcam",
|
||||
"disableWebcam": "Disable webcam"
|
||||
"disableWebcam": "Disable webcam",
|
||||
"defaultCamera": "Default Camera",
|
||||
"searching": "Searching...",
|
||||
"noneFound": "No camera found",
|
||||
"unavailable": "Camera unavailable"
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loading": "Loading sources...",
|
||||
|
||||
@@ -10,11 +10,16 @@
|
||||
"enableSystemAudio": "Activar audio del sistema",
|
||||
"disableSystemAudio": "Desactivar audio del sistema",
|
||||
"enableMicrophone": "Activar micrófono",
|
||||
"disableMicrophone": "Desactivar micrófono"
|
||||
"disableMicrophone": "Desactivar micrófono",
|
||||
"defaultMicrophone": "Micrófono predeterminado"
|
||||
},
|
||||
"webcam": {
|
||||
"enableWebcam": "Activar cámara web",
|
||||
"disableWebcam": "Desactivar cámara web"
|
||||
"disableWebcam": "Desactivar cámara web",
|
||||
"defaultCamera": "Cámara predeterminada",
|
||||
"searching": "Buscando...",
|
||||
"noneFound": "No se encontró cámara",
|
||||
"unavailable": "Cámara no disponible"
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loading": "Cargando fuentes...",
|
||||
|
||||
@@ -10,11 +10,16 @@
|
||||
"enableSystemAudio": "启用系统音频",
|
||||
"disableSystemAudio": "禁用系统音频",
|
||||
"enableMicrophone": "启用麦克风",
|
||||
"disableMicrophone": "禁用麦克风"
|
||||
"disableMicrophone": "禁用麦克风",
|
||||
"defaultMicrophone": "默认麦克风"
|
||||
},
|
||||
"webcam": {
|
||||
"enableWebcam": "启用摄像头",
|
||||
"disableWebcam": "禁用摄像头"
|
||||
"disableWebcam": "禁用摄像头",
|
||||
"defaultCamera": "默认摄像头",
|
||||
"searching": "正在搜索...",
|
||||
"noneFound": "未找到摄像头",
|
||||
"unavailable": "摄像头不可用"
|
||||
},
|
||||
"sourceSelector": {
|
||||
"loading": "正在加载源...",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { defineConfig } from "vitest/config";
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
environment: "jsdom",
|
||||
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
},
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user