mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0331416942 | ||
|
|
bb710e005c | ||
|
|
183af5117e | ||
|
|
95a0dcf389 | ||
|
|
3ab541549f | ||
|
|
1fbd7ac80f | ||
|
|
8213612135 | ||
|
|
fbd6aa0315 | ||
|
|
4448948f9e | ||
|
|
130cc27cf5 | ||
|
|
459d07004f | ||
|
|
6985eb3a2b | ||
|
|
f85cb947eb | ||
|
|
63e2b4c549 | ||
|
|
86f5b9888c | ||
|
|
2ad6125849 |
+1
-1
@@ -9,7 +9,7 @@ import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: ["@storybook/addon-docs"],
|
||||
addons: ["@storybook/addon-docs", "@storybook/addon-vitest"],
|
||||
framework: "@storybook/react-vite",
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -220,7 +220,7 @@ including federation:
|
||||
- Minimum TLS reverse proxy for
|
||||
- Synapse homeserver: `synapse.m.localhost` and `synapse.othersite.m.localhost`
|
||||
- MatrixRTC backend: `matrix-rtc.m.localhost` and `matrix-rtc.othersite.m.localhost`
|
||||
- Local Element Call development `call.m.localhost` via `yarn dev --host `
|
||||
- Local Element Call development `call.m.localhost` via `pnpm dev --host `
|
||||
- Element Web `app.m.localhost` and `app.othersite.m.localhost`
|
||||
- Note certificates will expire on Thr, 20 September 2035 14:27:35 CEST
|
||||
|
||||
|
||||
@@ -202,7 +202,6 @@
|
||||
"camera_numbered": "Camera {{n}}",
|
||||
"change_device_button": "Change audio device",
|
||||
"default": "Default",
|
||||
"default_named": "Default <2>({{name}})</2>",
|
||||
"handset": "Handset",
|
||||
"loudspeaker": "Loudspeaker",
|
||||
"microphone": "Microphone",
|
||||
|
||||
+11
-5
@@ -27,6 +27,8 @@
|
||||
"i18n": "i18next",
|
||||
"i18n:check": "i18next --fail-on-warnings --fail-on-update",
|
||||
"test": "vitest",
|
||||
"test:storybook": "vitest --project=storybook",
|
||||
"test:unit": "vitest --project=unit",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"backend": "docker-compose -f dev-backend-docker-compose.yml up",
|
||||
"backend-playwright": "docker-compose -f playwright-backend-docker-compose.yml -f playwright-backend-docker-compose.override.yml up",
|
||||
@@ -59,8 +61,9 @@
|
||||
"@react-spring/web": "^10.0.0",
|
||||
"@sentry/react": "^8.0.0",
|
||||
"@sentry/vite-plugin": "^3.0.0",
|
||||
"@storybook/addon-docs": "^10.3.3",
|
||||
"@storybook/react-vite": "^10.3.3",
|
||||
"@storybook/addon-docs": "^10.3.6",
|
||||
"@storybook/addon-vitest": "^10.3.6",
|
||||
"@storybook/react-vite": "^10.3.6",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
"@testing-library/dom": "^10.1.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
@@ -83,7 +86,9 @@
|
||||
"@vector-im/compound-design-tokens": "^10.0.0",
|
||||
"@vector-im/compound-web": "^9.3.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"@vitest/browser-playwright": "^4.1.5",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@vitest/ui": "4.1.5",
|
||||
"babel-plugin-transform-vite-meta-env": "^1.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
@@ -98,7 +103,7 @@
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-rxjs": "^5.0.3",
|
||||
"eslint-plugin-storybook": "^10.3.3",
|
||||
"eslint-plugin-storybook": "^10.3.6",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fetch-mock": "11.1.5",
|
||||
"global-jsdom": "^26.0.0",
|
||||
@@ -116,6 +121,7 @@
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
"pako": "^2.0.4",
|
||||
"playwright": "^1.59.1",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
"posthog-js": "1.160.3",
|
||||
@@ -128,7 +134,7 @@
|
||||
"react-use-measure": "^2.1.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sass": "^1.42.1",
|
||||
"storybook": "^10.3.3",
|
||||
"storybook": "^10.3.6",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint-language-service": "^5.0.5",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
@@ -141,7 +147,7 @@
|
||||
"vite-plugin-node-stdlib-browser": "^0.2.1",
|
||||
"vite-plugin-svgr": "^4.0.0",
|
||||
"vite-plugin-wasm": "^3.6.0",
|
||||
"vitest": "^4.0.18",
|
||||
"vitest": "^4.1.5",
|
||||
"vitest-axe": "^1.0.0-pre.3"
|
||||
},
|
||||
"pnpm": {
|
||||
|
||||
Generated
+211
-60
@@ -82,11 +82,14 @@ importers:
|
||||
specifier: ^3.0.0
|
||||
version: 3.6.1
|
||||
'@storybook/addon-docs':
|
||||
specifier: ^10.3.3
|
||||
version: 10.3.5(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
specifier: ^10.3.6
|
||||
version: 10.3.6(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
'@storybook/addon-vitest':
|
||||
specifier: ^10.3.6
|
||||
version: 10.3.6(@vitest/browser-playwright@4.1.5)(@vitest/browser@4.1.5)(@vitest/runner@4.1.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vitest@4.1.5)
|
||||
'@storybook/react-vite':
|
||||
specifier: ^10.3.3
|
||||
version: 10.3.5(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
specifier: ^10.3.6
|
||||
version: 10.3.6(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
'@stylistic/eslint-plugin':
|
||||
specifier: ^3.0.0
|
||||
version: 3.1.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
@@ -153,8 +156,14 @@ importers:
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.0.1
|
||||
version: 4.7.0(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
'@vitest/browser-playwright':
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.18
|
||||
version: 4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5)
|
||||
'@vitest/ui':
|
||||
specifier: 4.1.5
|
||||
version: 4.1.5(vitest@4.1.5)
|
||||
babel-plugin-transform-vite-meta-env:
|
||||
specifier: ^1.0.3
|
||||
@@ -199,8 +208,8 @@ importers:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint-plugin-storybook:
|
||||
specifier: ^10.3.3
|
||||
version: 10.3.5(eslint@8.57.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)
|
||||
specifier: ^10.3.6
|
||||
version: 10.3.6(eslint@8.57.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)
|
||||
eslint-plugin-unicorn:
|
||||
specifier: ^56.0.0
|
||||
version: 56.0.1(eslint@8.57.1)
|
||||
@@ -252,6 +261,9 @@ importers:
|
||||
pako:
|
||||
specifier: ^2.0.4
|
||||
version: 2.1.0
|
||||
playwright:
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
postcss:
|
||||
specifier: ^8.4.41
|
||||
version: 8.5.11
|
||||
@@ -289,8 +301,8 @@ importers:
|
||||
specifier: ^1.42.1
|
||||
version: 1.99.0
|
||||
storybook:
|
||||
specifier: ^10.3.3
|
||||
version: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
specifier: ^10.3.6
|
||||
version: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.9.3
|
||||
@@ -328,8 +340,8 @@ importers:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
vitest-axe:
|
||||
specifier: ^1.0.0-pre.3
|
||||
version: 1.0.0-pre.5(vitest@4.1.5)
|
||||
@@ -941,6 +953,9 @@ packages:
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@blazediff/core@1.9.1':
|
||||
resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==}
|
||||
|
||||
'@bufbuild/protobuf@1.10.1':
|
||||
resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==}
|
||||
|
||||
@@ -1874,6 +1889,9 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@radix-ui/number@1.1.1':
|
||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||
|
||||
@@ -2679,23 +2697,41 @@ packages:
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@storybook/addon-docs@10.3.5':
|
||||
resolution: {integrity: sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA==}
|
||||
'@storybook/addon-docs@10.3.6':
|
||||
resolution: {integrity: sha512-TvIdADVPtauxW0LzXIpIv7X6GxwetorhyNh+6+7MHC27XSBCWVxxRUwL63YeLlHTuXsIk0quG3b1xgwVRzWOJA==}
|
||||
peerDependencies:
|
||||
storybook: ^10.3.5
|
||||
storybook: ^10.3.6
|
||||
|
||||
'@storybook/builder-vite@10.3.5':
|
||||
resolution: {integrity: sha512-i4KwCOKbhtlbQIbhm53+Kk7bMnxa0cwTn1pxmtA/x5wm1Qu7FrrBQV0V0DNjkUqzcSKo1CjspASJV/HlY0zYlw==}
|
||||
'@storybook/addon-vitest@10.3.6':
|
||||
resolution: {integrity: sha512-HXj7RrPJY+xzoNjL+xZu2oLw1fI5BA87Noh1NAXMPuECHR5R5fuRM/tTsJuIGXHFMO06FjSi/rekDIfCj1fL4w==}
|
||||
peerDependencies:
|
||||
storybook: ^10.3.5
|
||||
'@vitest/browser': ^3.0.0 || ^4.0.0
|
||||
'@vitest/browser-playwright': ^4.0.0
|
||||
'@vitest/runner': ^3.0.0 || ^4.0.0
|
||||
storybook: ^10.3.6
|
||||
vitest: ^3.0.0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/browser-playwright':
|
||||
optional: true
|
||||
'@vitest/runner':
|
||||
optional: true
|
||||
vitest:
|
||||
optional: true
|
||||
|
||||
'@storybook/builder-vite@10.3.6':
|
||||
resolution: {integrity: sha512-gpvR/sE4BcrFtmQZ+Ker7zD23oQzoVeqD9nF6cK6yzY+Q0svJXyX2EPmFG4y+EwygD5/vNzDpP84gGMut8VRwg==}
|
||||
peerDependencies:
|
||||
storybook: ^10.3.6
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
'@storybook/csf-plugin@10.3.5':
|
||||
resolution: {integrity: sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw==}
|
||||
'@storybook/csf-plugin@10.3.6':
|
||||
resolution: {integrity: sha512-9kBf7VRdRqTSIYo+rPtVn5yjYYyK8kP2QhEYx3oiXvfwy4RexmbJnhk/tXa/lNiTqukA1TqaWQ2+5MqF4fu6YQ==}
|
||||
peerDependencies:
|
||||
esbuild: ^0.28.0
|
||||
rollup: '*'
|
||||
storybook: ^10.3.5
|
||||
storybook: ^10.3.6
|
||||
vite: '*'
|
||||
webpack: '*'
|
||||
peerDependenciesMeta:
|
||||
@@ -2717,27 +2753,27 @@ packages:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@storybook/react-dom-shim@10.3.5':
|
||||
resolution: {integrity: sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g==}
|
||||
'@storybook/react-dom-shim@10.3.6':
|
||||
resolution: {integrity: sha512-/Tu1gPu+Fw+zOnAGmxRmOD30FX3a04LxcTAKflEtdpmtIMVR5bA3qpjy+f5YhoyDCecbXyKmL1OeIU2FIIZHqQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
storybook: ^10.3.5
|
||||
storybook: ^10.3.6
|
||||
|
||||
'@storybook/react-vite@10.3.5':
|
||||
resolution: {integrity: sha512-UB5sJHeh26bfd8sNMx2YPGYRYmErIdTRaLOT28m4bykQIa1l9IgVktsYg/geW7KsJU0lXd3oTbnUjLD+enpi3w==}
|
||||
'@storybook/react-vite@10.3.6':
|
||||
resolution: {integrity: sha512-tySQRc+8q7V2NkylQMNJjDV8zXy6tkxb8oDqw/DIhHhI9Xn77MTKVZ8Cihbo5NMm7HYTB6xDKr6wqdSMgdufYQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
storybook: ^10.3.5
|
||||
storybook: ^10.3.6
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
'@storybook/react@10.3.5':
|
||||
resolution: {integrity: sha512-tpLTLaVGoA6fLK3ReyGzZUricq7lyPaV2hLPpj5wqdXLV/LpRtAHClUpNoPDYSBjlnSjL81hMZijbkGC3mA+gw==}
|
||||
'@storybook/react@10.3.6':
|
||||
resolution: {integrity: sha512-oZQZ6xayWe5IdHmFUTL0TL8rX/gpNNh9gWhT2vzW5eeUvlkVG/RBKdsja6Ndrk2s1D9vcnwiI6r6CNXy3IEEmg==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
storybook: ^10.3.5
|
||||
storybook: ^10.3.6
|
||||
typescript: '>= 4.9.x'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
@@ -3173,6 +3209,17 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
'@vitest/browser-playwright@4.1.5':
|
||||
resolution: {integrity: sha512-CWy0lBQJq97nionyJJdnaU4961IXTl43a7UCu5nHy51IoKxAt6PVIJLo+76rVl7KOOgcWHNkG4kbJu/pW7knvA==}
|
||||
peerDependencies:
|
||||
playwright: '*'
|
||||
vitest: 4.1.5
|
||||
|
||||
'@vitest/browser@4.1.5':
|
||||
resolution: {integrity: sha512-iCDGI8c4yg+xmjUg2VsygdAUSIIB4x5Rht/P68OXy1hPELKXHDkzh87lkuTcdYmemRChDkEpB426MmDjzC0ziA==}
|
||||
peerDependencies:
|
||||
vitest: 4.1.5
|
||||
|
||||
'@vitest/coverage-v8@4.1.5':
|
||||
resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==}
|
||||
peerDependencies:
|
||||
@@ -3217,6 +3264,11 @@ packages:
|
||||
'@vitest/spy@4.1.5':
|
||||
resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==}
|
||||
|
||||
'@vitest/ui@4.1.5':
|
||||
resolution: {integrity: sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==}
|
||||
peerDependencies:
|
||||
vitest: 4.1.5
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
|
||||
|
||||
@@ -4165,11 +4217,11 @@ packages:
|
||||
eslint: ^8.0.0
|
||||
typescript: '>=4.0.0'
|
||||
|
||||
eslint-plugin-storybook@10.3.5:
|
||||
resolution: {integrity: sha512-rEFkfU3ypF44GpB4tiJ9EFDItueoGvGi3+weLHZax2ON2MB7VIDsxdSUGvIU5tMURg+oWYlpzCyLm4TpDq2deA==}
|
||||
eslint-plugin-storybook@10.3.6:
|
||||
resolution: {integrity: sha512-8udrL+Rmp5LFaZvgRe4J226X1MYls25bWCyHuzR5X8s2qbFTryX+wKC+o/0Ato4A1AvwnDg8OOMPc6yWJ9JpcA==}
|
||||
peerDependencies:
|
||||
eslint: '>=8'
|
||||
storybook: ^10.3.5
|
||||
storybook: ^10.3.6
|
||||
|
||||
eslint-plugin-unicorn@56.0.1:
|
||||
resolution: {integrity: sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==}
|
||||
@@ -4311,6 +4363,9 @@ packages:
|
||||
fflate@0.4.8:
|
||||
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
|
||||
file-entry-cache@6.0.1:
|
||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
@@ -5154,6 +5209,10 @@ packages:
|
||||
resolution: {integrity: sha512-Q9wJ/xhzeD9Wua1MwDN2v3ah3HENsUVSlzzL9Qw149cL9hHZkXtQGl3Eq36BbdLV+/qUwaP1WtJQ+H/+Oxso8g==}
|
||||
engines: {node: 20 || 22 || 24}
|
||||
|
||||
mrmime@2.0.1:
|
||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
@@ -5429,6 +5488,10 @@ packages:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
pngjs@7.0.0:
|
||||
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
|
||||
engines: {node: '>=14.19.0'}
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6059,6 +6122,10 @@ packages:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
sirv@3.0.2:
|
||||
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
@@ -6116,14 +6183,17 @@ packages:
|
||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
storybook@10.3.5:
|
||||
resolution: {integrity: sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw==}
|
||||
storybook@10.3.6:
|
||||
resolution: {integrity: sha512-vbSz7g/1rGMC1uAULqMZjALkIuLu2QABqfhRYhyr/11kzyesi+vAmwyJLukZP1FfecxGOgMwOh6GS0YsGpHAvQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
prettier: ^2 || ^3
|
||||
vite-plus: ^0.1.15
|
||||
peerDependenciesMeta:
|
||||
prettier:
|
||||
optional: true
|
||||
vite-plus:
|
||||
optional: true
|
||||
|
||||
stream-browserify@3.0.0:
|
||||
resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==}
|
||||
@@ -6295,6 +6365,10 @@ packages:
|
||||
toggle-selection@1.0.6:
|
||||
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
||||
|
||||
totalist@3.0.1:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -7634,6 +7708,8 @@ snapshots:
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@blazediff/core@1.9.1': {}
|
||||
|
||||
'@bufbuild/protobuf@1.10.1': {}
|
||||
|
||||
'@codecov/bundler-plugin-core@1.9.1':
|
||||
@@ -8473,6 +8549,8 @@ snapshots:
|
||||
dependencies:
|
||||
playwright: 1.59.1
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@radix-ui/number@1.1.1': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
@@ -9139,15 +9217,15 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@storybook/addon-docs@10.3.5(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
|
||||
'@storybook/addon-docs@10.3.6(@types/react@19.2.14)(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5)
|
||||
'@storybook/csf-plugin': 10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
'@storybook/csf-plugin': 10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
'@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
|
||||
'@storybook/react-dom-shim': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
|
||||
react: 19.2.5
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
ts-dedent: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
@@ -9156,10 +9234,24 @@ snapshots:
|
||||
- vite
|
||||
- webpack
|
||||
|
||||
'@storybook/builder-vite@10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
|
||||
'@storybook/addon-vitest@10.3.6(@vitest/browser-playwright@4.1.5)(@vitest/browser@4.1.5)(@vitest/runner@4.1.5)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vitest@4.1.5)':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@storybook/global': 5.0.0
|
||||
'@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
optionalDependencies:
|
||||
'@vitest/browser': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
|
||||
'@vitest/browser-playwright': 4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
|
||||
'@vitest/runner': 4.1.5
|
||||
vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
- react-dom
|
||||
|
||||
'@storybook/builder-vite@10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
ts-dedent: 2.2.0
|
||||
vite: 8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
@@ -9167,9 +9259,9 @@ snapshots:
|
||||
- rollup
|
||||
- webpack
|
||||
|
||||
'@storybook/csf-plugin@10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
|
||||
'@storybook/csf-plugin@10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
esbuild: 0.28.0
|
||||
@@ -9183,25 +9275,25 @@ snapshots:
|
||||
react: 19.2.5
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
|
||||
'@storybook/react-dom-shim@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
|
||||
'@storybook/react-dom-shim@10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))':
|
||||
dependencies:
|
||||
react: 19.2.5
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
|
||||
'@storybook/react-vite@10.3.5(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
|
||||
'@storybook/react-vite@10.3.6(esbuild@0.28.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@storybook/builder-vite': 10.3.5(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
'@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)
|
||||
'@storybook/builder-vite': 10.3.6(esbuild@0.28.0)(rollup@4.60.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
'@storybook/react': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)
|
||||
empathic: 2.0.0
|
||||
magic-string: 0.30.21
|
||||
react: 19.2.5
|
||||
react-docgen: 8.0.3
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
resolve: 1.22.12
|
||||
storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
tsconfig-paths: 4.2.0
|
||||
vite: 8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
@@ -9211,15 +9303,15 @@ snapshots:
|
||||
- typescript
|
||||
- webpack
|
||||
|
||||
'@storybook/react@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)':
|
||||
'@storybook/react@10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
'@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
|
||||
'@storybook/react-dom-shim': 10.3.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))
|
||||
react: 19.2.5
|
||||
react-docgen: 8.0.3
|
||||
react-docgen-typescript: 2.4.0(typescript@5.9.3)
|
||||
react-dom: 19.2.5(react@19.2.5)
|
||||
storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -9745,7 +9837,37 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@4.1.5(vitest@4.1.5)':
|
||||
'@vitest/browser-playwright@4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
|
||||
'@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
playwright: 1.59.1
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
|
||||
'@vitest/browser@4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)':
|
||||
dependencies:
|
||||
'@blazediff/core': 1.9.1
|
||||
'@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
'@vitest/utils': 4.1.5
|
||||
magic-string: 0.30.21
|
||||
pngjs: 7.0.0
|
||||
sirv: 3.0.2
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
ws: 8.20.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
|
||||
'@vitest/coverage-v8@4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5)':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.1.5
|
||||
@@ -9757,7 +9879,9 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 4.1.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
optionalDependencies:
|
||||
'@vitest/browser': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
|
||||
|
||||
'@vitest/expect@3.2.4':
|
||||
dependencies:
|
||||
@@ -9810,6 +9934,17 @@ snapshots:
|
||||
|
||||
'@vitest/spy@4.1.5': {}
|
||||
|
||||
'@vitest/ui@4.1.5(vitest@4.1.5)':
|
||||
dependencies:
|
||||
'@vitest/utils': 4.1.5
|
||||
fflate: 0.8.2
|
||||
flatted: 3.4.2
|
||||
pathe: 2.0.3
|
||||
sirv: 3.0.2
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
@@ -10966,11 +11101,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-storybook@10.3.5(eslint@8.57.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3):
|
||||
eslint-plugin-storybook@10.3.6(eslint@8.57.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.58.2(eslint@8.57.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.59.1(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
storybook: 10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -11151,6 +11286,8 @@ snapshots:
|
||||
|
||||
fflate@0.4.8: {}
|
||||
|
||||
fflate@0.8.2: {}
|
||||
|
||||
file-entry-cache@6.0.1:
|
||||
dependencies:
|
||||
flat-cache: 3.2.0
|
||||
@@ -12051,6 +12188,8 @@ snapshots:
|
||||
|
||||
mktemp@2.0.2: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
@@ -12381,6 +12520,8 @@ snapshots:
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
pngjs@7.0.0: {}
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-attribute-case-insensitive@7.0.1(postcss@8.5.11):
|
||||
@@ -13141,6 +13282,12 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
sirv@3.0.2:
|
||||
dependencies:
|
||||
'@polka/url': 1.0.0-next.29
|
||||
mrmime: 2.0.1
|
||||
totalist: 3.0.1
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
slash@3.0.0: {}
|
||||
@@ -13195,7 +13342,7 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
internal-slot: 1.1.0
|
||||
|
||||
storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||
storybook@10.3.6(@testing-library/dom@10.4.1)(prettier@3.8.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
'@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
@@ -13429,6 +13576,8 @@ snapshots:
|
||||
|
||||
toggle-selection@1.0.6: {}
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tough-cookie@5.1.2:
|
||||
dependencies:
|
||||
tldts: 6.1.86
|
||||
@@ -13788,9 +13937,9 @@ snapshots:
|
||||
axe-core: 4.11.3
|
||||
chalk: 5.6.2
|
||||
lodash-es: 4.18.1
|
||||
vitest: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
vitest: 4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
|
||||
vitest@4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)):
|
||||
vitest@4.1.5(@types/node@24.12.2)(@vitest/browser-playwright@4.1.5)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.5
|
||||
'@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))
|
||||
@@ -13814,7 +13963,9 @@ snapshots:
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.12.2
|
||||
'@vitest/coverage-v8': 4.1.5(vitest@4.1.5)
|
||||
'@vitest/browser-playwright': 4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.5)
|
||||
'@vitest/coverage-v8': 4.1.5(@vitest/browser@4.1.5)(vitest@4.1.5)
|
||||
'@vitest/ui': 4.1.5(vitest@4.1.5)
|
||||
jsdom: 26.1.0
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
@@ -26,10 +26,6 @@ Please see LICENSE in the repository root for full details.
|
||||
);
|
||||
}
|
||||
|
||||
.footer.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer.overlay {
|
||||
/* Note that the footer is still position: sticky in this case so that certain
|
||||
tiles can move up out of the way of the footer when visible. */
|
||||
|
||||
@@ -5,20 +5,41 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { fn } from "storybook/test";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { type ReactNode } from "react";
|
||||
import { type JSX, type ReactNode } from "react";
|
||||
import { Link } from "@vector-im/compound-web";
|
||||
import {
|
||||
useArgs,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from "storybook/internal/preview-api";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { CallFooter, type FooterProps } from "./CallFooter";
|
||||
import { CallFooter, type FooterSnapshot } from "./CallFooter";
|
||||
import inCallViewStyles from "../room/InCallView.module.css";
|
||||
import { createMockedViewModel } from "../state/ViewModel";
|
||||
import { ReactionsSenderContext } from "../reactions/useReactionsSender";
|
||||
import { type ReactionOption } from "../reactions";
|
||||
import { GridMode } from "../state/CallViewModel/CallViewModel";
|
||||
import { constant } from "../state/Behavior";
|
||||
|
||||
function CallFooterWrapper(props: FooterProps): ReactNode {
|
||||
/**
|
||||
* A wrapper component that is used for:
|
||||
* - exposing the snapshot via props so the storybook documents the snapshot properties (basically unpack them form the vm)
|
||||
* - Add additional react context
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
function CallFooterStoryWrapper(
|
||||
props: FooterSnapshot & {
|
||||
children?: false | JSX.Element | JSX.Element[] | undefined;
|
||||
},
|
||||
): ReactNode {
|
||||
const { children, ...vmProps } = props;
|
||||
const vm = createMockedViewModel(vmProps);
|
||||
return (
|
||||
<div className={inCallViewStyles.inRoom}>
|
||||
<div /*className={inCallViewStyles.inRoom}*/>
|
||||
<ReactionsSenderContext
|
||||
value={{
|
||||
supportsReactions: false,
|
||||
@@ -26,15 +47,15 @@ function CallFooterWrapper(props: FooterProps): ReactNode {
|
||||
sendReaction: async (reaction: ReactionOption) => Promise.resolve(),
|
||||
}}
|
||||
>
|
||||
<CallFooter {...props} />
|
||||
<CallFooter vm={vm} />
|
||||
</ReactionsSenderContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
component: CallFooterWrapper,
|
||||
} satisfies Meta<typeof CallFooterWrapper>;
|
||||
component: CallFooterStoryWrapper,
|
||||
} satisfies Meta<typeof CallFooterStoryWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
@@ -50,9 +71,10 @@ const fnArgType = {
|
||||
options: ["MockedCallback", "undefined"],
|
||||
mapping: { MockedCallback: fn(), undefined: undefined },
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
hideLogo: true,
|
||||
showLogo: false,
|
||||
layoutMode: "grid",
|
||||
audioEnabled: true,
|
||||
videoEnabled: true,
|
||||
@@ -62,6 +84,7 @@ export const Default: Story = {
|
||||
toggleVideo: fn(),
|
||||
toggleScreenSharing: fn(),
|
||||
hangup: fn(),
|
||||
buttonSize: "lg",
|
||||
},
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
@@ -88,11 +111,29 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAudioAndVideoOptions: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
audioEnabled: false,
|
||||
videoEnabled: true,
|
||||
audioOptions: [
|
||||
{ label: "Microphone 1", id: "1" },
|
||||
{ label: "Microphone 2", id: "2" },
|
||||
],
|
||||
videoOptions: [
|
||||
{ label: "Camera 1", id: "1" },
|
||||
{ label: "Camera 2", id: "2" },
|
||||
],
|
||||
selectedAudio: "2",
|
||||
selectedVideo: "1",
|
||||
},
|
||||
};
|
||||
export const WithLogo: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
hideLogo: false,
|
||||
showLogo: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -103,6 +144,35 @@ export const AudioVideoEnabled: Story = {
|
||||
audioEnabled: true,
|
||||
videoEnabled: true,
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const spotlightRadio = canvas.getByRole("radio", { name: "Spotlight" });
|
||||
await userEvent.click(spotlightRadio);
|
||||
await expect(args.setLayoutMode).toHaveBeenCalledWith("spotlight");
|
||||
|
||||
const micButtonMute = canvas.getByRole("switch", {
|
||||
name: "Mute microphone",
|
||||
});
|
||||
await userEvent.click(micButtonMute);
|
||||
await expect(args.toggleAudio).toHaveBeenCalled();
|
||||
|
||||
const videoMuteButton = canvas.getByRole("switch", {
|
||||
name: "Stop video",
|
||||
});
|
||||
await userEvent.click(videoMuteButton);
|
||||
await expect(args.toggleVideo).toHaveBeenCalled();
|
||||
const screenShare = canvas.getByRole("switch", {
|
||||
name: "Share screen",
|
||||
});
|
||||
await userEvent.click(screenShare);
|
||||
await expect(args.toggleScreenSharing).toHaveBeenCalled();
|
||||
const endCall = canvas.getByRole("button", {
|
||||
name: "End call",
|
||||
});
|
||||
await userEvent.click(endCall);
|
||||
await expect(args.hangup).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAudioOutputSpeaker: Story = {
|
||||
@@ -132,7 +202,38 @@ export const Pip: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
asPip: true,
|
||||
buttonSize: "md",
|
||||
showSettingsButton: false,
|
||||
layoutMode: undefined,
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await expect(
|
||||
canvas.getByRole("radio", { name: "Spotlight" }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const micButtonMute = canvas.getByRole("switch", {
|
||||
name: "Mute microphone",
|
||||
});
|
||||
await userEvent.click(micButtonMute);
|
||||
await expect(args.toggleAudio).toHaveBeenCalled();
|
||||
|
||||
const videoMuteButton = canvas.getByRole("switch", {
|
||||
name: "Stop video",
|
||||
});
|
||||
await userEvent.click(videoMuteButton);
|
||||
await expect(args.toggleVideo).toHaveBeenCalled();
|
||||
const screenShare = canvas.getByRole("switch", {
|
||||
name: "Share screen",
|
||||
});
|
||||
await userEvent.click(screenShare);
|
||||
await expect(args.toggleScreenSharing).toHaveBeenCalled();
|
||||
const endCall = canvas.getByRole("button", {
|
||||
name: "End call",
|
||||
});
|
||||
await userEvent.click(endCall);
|
||||
await expect(args.hangup).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
export const NoControlsWithLogo: Story = {
|
||||
@@ -140,7 +241,7 @@ export const NoControlsWithLogo: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
hideControls: true,
|
||||
hideLogo: false,
|
||||
showLogo: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -169,7 +270,7 @@ export const MobileLayout: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
hideLogo: true,
|
||||
showLogo: false,
|
||||
|
||||
audioOutputSwitcher: { targetOutput: "speaker", switch: fn() },
|
||||
},
|
||||
@@ -185,7 +286,7 @@ export const Lobby: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
hideLogo: true,
|
||||
showLogo: false,
|
||||
openSettings: undefined,
|
||||
setLayoutMode: undefined,
|
||||
toggleScreenSharing: undefined,
|
||||
@@ -199,7 +300,7 @@ export const LobbyMobile: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
hideLogo: true,
|
||||
showLogo: false,
|
||||
|
||||
setLayoutMode: undefined,
|
||||
toggleScreenSharing: undefined,
|
||||
@@ -217,7 +318,7 @@ export const LobbyRecentButton: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
children: <Link>Back To Recents</Link>,
|
||||
hideLogo: true,
|
||||
showLogo: false,
|
||||
setLayoutMode: undefined,
|
||||
toggleScreenSharing: undefined,
|
||||
},
|
||||
@@ -231,7 +332,7 @@ export const LobbyRecentButtonMobile: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
children: <Link>Back To Recents</Link>,
|
||||
hideLogo: true,
|
||||
showLogo: false,
|
||||
setLayoutMode: undefined,
|
||||
toggleScreenSharing: undefined,
|
||||
},
|
||||
|
||||
+396
-74
@@ -7,13 +7,13 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { type FC, type JSX, type Ref, useMemo } from "react";
|
||||
import classNames from "classnames";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Switch } from "@vector-im/compound-web";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
SpotlightIcon,
|
||||
GridIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { combineLatest, map } from "rxjs";
|
||||
import { supportsBackgroundProcessors } from "@livekit/track-processors";
|
||||
import { Switch } from "@vector-im/compound-web";
|
||||
|
||||
import LogoMark from "../icons/LogoMark.svg?react";
|
||||
import LogoType from "../icons/LogoType.svg?react";
|
||||
@@ -29,35 +29,54 @@ import {
|
||||
type ReactionData,
|
||||
} from "../button";
|
||||
import styles from "./CallFooter.module.css";
|
||||
import { type GridMode } from "../state/CallViewModel/CallViewModel";
|
||||
import {
|
||||
type CallViewModel,
|
||||
type GridMode,
|
||||
} from "../state/CallViewModel/CallViewModel";
|
||||
import {
|
||||
MediaMuteAndSwitchButton,
|
||||
type MenuOptions,
|
||||
type ToggleOption,
|
||||
} from "./MediaMuteAndSwitchButton";
|
||||
import { type MediaDevices } from "../state/MediaDevices";
|
||||
import { mediaDeviceLabelToString } from "../settings/DeviceSelection";
|
||||
import {
|
||||
backgroundBlur as backgroundBlurSettings,
|
||||
debugTileLayout as debugTileLayoutSetting,
|
||||
} from "../settings/settings";
|
||||
import { constant } from "../state/Behavior";
|
||||
import type { ObservableScope } from "../state/ObservableScope";
|
||||
import { type MuteStates } from "../state/MuteStates";
|
||||
import { type ViewModel, useViewModel } from "../state/ViewModel";
|
||||
import { getUrlParams, HeaderStyle } from "../UrlParams";
|
||||
import { t } from "i18next";
|
||||
|
||||
export interface AudioOutputSwitcher {
|
||||
targetOutput: string;
|
||||
switch: () => void;
|
||||
}
|
||||
|
||||
export interface FooterProps {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
/** Children will only be visible if the component is wider than 5*/
|
||||
children?: JSX.Element | JSX.Element[] | false;
|
||||
|
||||
export interface FooterSnapshot {
|
||||
audioEnabled: boolean;
|
||||
/** Also controls if the audioMute button is disabled */
|
||||
toggleAudio: (() => void) | undefined;
|
||||
|
||||
videoEnabled: boolean;
|
||||
/** Also controls if the videoMute button is disabled */
|
||||
toggleVideo: (() => void) | undefined;
|
||||
|
||||
/* This is needed for WindowMode = "flat" */
|
||||
hideControls?: boolean;
|
||||
/** hide the entire footer*/
|
||||
hidden?: boolean;
|
||||
/** Pip controls buttonSize and hides: settings button, layout switcher and logo */
|
||||
asPip?: boolean;
|
||||
/** The footer should be used as an overlay.
|
||||
* (Over the Call Grid) This saves spaces on small screens.*/
|
||||
* (Over the Call Grid) This saves spaces on small screens. */
|
||||
asOverlay?: boolean;
|
||||
|
||||
buttonSize: "md" | "lg";
|
||||
showSettingsButton?: boolean;
|
||||
showLayoutSwitcher?: boolean;
|
||||
showLogoDebugContainer?: boolean;
|
||||
showLogo?: boolean;
|
||||
|
||||
layoutMode?: GridMode;
|
||||
/** Also controls if the layout button is visible */
|
||||
setLayoutMode?: (mode: GridMode) => void;
|
||||
@@ -65,7 +84,7 @@ export interface FooterProps {
|
||||
sharingScreen?: boolean;
|
||||
toggleScreenSharing?: () => void;
|
||||
|
||||
/** Also controls if the audio button is visible */
|
||||
/** Also controls if the audio output button is visible */
|
||||
audioOutputSwitcher?: AudioOutputSwitcher;
|
||||
/** Also controls if the settings button is visible */
|
||||
openSettings?: () => void;
|
||||
@@ -75,46 +94,319 @@ export interface FooterProps {
|
||||
reactionIdentifier?: string;
|
||||
reactionData?: ReactionData;
|
||||
|
||||
hideLogo?: boolean;
|
||||
// debug stuff
|
||||
debugTileLayout?: boolean;
|
||||
tileStoreGeneration?: number;
|
||||
|
||||
audioOptions?: MenuOptions[];
|
||||
videoOptions?: MenuOptions[];
|
||||
selectedAudio?: string;
|
||||
selectedVideo?: string;
|
||||
selectAudioButtonOption?: (deviceId: string) => void;
|
||||
selectVideoButtonOption?: (option: string) => void;
|
||||
videoToggles?: ToggleOption[];
|
||||
}
|
||||
|
||||
export const CallFooter: FC<FooterProps> = ({
|
||||
ref,
|
||||
children,
|
||||
asOverlay,
|
||||
hidden,
|
||||
hideControls,
|
||||
hideLogo,
|
||||
asPip,
|
||||
layoutMode,
|
||||
setLayoutMode,
|
||||
openSettings,
|
||||
audioEnabled,
|
||||
videoEnabled,
|
||||
toggleAudio,
|
||||
toggleVideo,
|
||||
sharingScreen,
|
||||
toggleScreenSharing,
|
||||
reactionIdentifier,
|
||||
reactionData,
|
||||
audioOutputSwitcher,
|
||||
hangup,
|
||||
debugTileLayout,
|
||||
tileStoreGeneration,
|
||||
}) => {
|
||||
/**
|
||||
* Shared helper: maps MuteStates into the audio/video enabled + toggle behaviors
|
||||
* needed by FooterSnapshot.
|
||||
*/
|
||||
function buildMuteBehaviors(
|
||||
scope: ObservableScope,
|
||||
muteStates: MuteStates,
|
||||
): Pick<
|
||||
ViewModel<FooterSnapshot>,
|
||||
"audioEnabled" | "toggleAudio" | "videoEnabled" | "toggleVideo"
|
||||
> {
|
||||
return {
|
||||
audioEnabled: muteStates.audio.enabled$,
|
||||
toggleAudio: scope.behavior(
|
||||
muteStates.audio.toggle$.pipe(map((t) => t ?? undefined)),
|
||||
),
|
||||
videoEnabled: muteStates.video.enabled$,
|
||||
toggleVideo: scope.behavior(
|
||||
muteStates.video.toggle$.pipe(map((t) => t ?? undefined)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared helper: maps MediaDevices into the audio/video device-list behaviors
|
||||
* needed by FooterSnapshot (options, selection, callbacks, blur toggle).
|
||||
*/
|
||||
function buildDeviceBehaviors(
|
||||
scope: ObservableScope,
|
||||
mediaDevices: MediaDevices,
|
||||
): Pick<
|
||||
ViewModel<FooterSnapshot>,
|
||||
| "audioOptions"
|
||||
| "selectedAudio"
|
||||
| "selectAudioButtonOption"
|
||||
| "videoOptions"
|
||||
| "selectedVideo"
|
||||
| "selectVideoButtonOption"
|
||||
| "videoToggles"
|
||||
> {
|
||||
return {
|
||||
audioOptions: scope.behavior(
|
||||
mediaDevices.audioInput.available$.pipe(
|
||||
map((available) =>
|
||||
[...available.entries()].map(([id, label]) => ({
|
||||
id,
|
||||
label: mediaDeviceLabelToString(label, (n) => "Audio Device " + n),
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedAudio: scope.behavior(
|
||||
mediaDevices.audioInput.selected$.pipe(map((s) => s?.id)),
|
||||
),
|
||||
selectAudioButtonOption: constant(mediaDevices.audioInput.select),
|
||||
videoOptions: scope.behavior(
|
||||
mediaDevices.videoInput.available$.pipe(
|
||||
map((available) =>
|
||||
[...available.entries()].map(([id, label]) => ({
|
||||
id,
|
||||
label: mediaDeviceLabelToString(label, (n) => "Camera " + n),
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
selectedVideo: scope.behavior(
|
||||
mediaDevices.videoInput.selected$.pipe(map((s) => s?.id)),
|
||||
),
|
||||
selectVideoButtonOption: scope.behavior(
|
||||
backgroundBlurSettings.value$.pipe(
|
||||
map((current) => {
|
||||
return (option: string) => {
|
||||
if (option === "blur") {
|
||||
backgroundBlurSettings.setValue(!current);
|
||||
} else {
|
||||
mediaDevices.videoInput.select(option);
|
||||
}
|
||||
};
|
||||
}),
|
||||
),
|
||||
),
|
||||
videoToggles: scope.behavior(
|
||||
backgroundBlurSettings.value$.pipe(
|
||||
map((blurActive) =>
|
||||
supportsBackgroundProcessors()
|
||||
? [{ id: "blur", enabled: blurActive, label: "Blur Background" }]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the ViewModel for the CallFooter.
|
||||
*
|
||||
* @param scope - ObservableScope that bounds the lifetime of derived behaviors.
|
||||
* @param vm - The root CallViewModel; provides layout, grid mode, reactions, etc.
|
||||
* @param muteStates - Audio and video mute state + toggles.
|
||||
* @param mediaDevices - Available and selected input devices.
|
||||
* @param openSettings - Callback to open the settings modal, or undefined if the
|
||||
* settings button should be hidden (e.g. when it is already shown in an app bar).
|
||||
* @param hideControls - When true the button row is hidden (from URL param).
|
||||
* @param reactionIdentifier - The local user's reaction identifier string, or
|
||||
* undefined when reactions are not supported (hides the reaction button).
|
||||
*/
|
||||
export function createCallFooterViewModel(
|
||||
scope: ObservableScope,
|
||||
callModel: CallViewModel,
|
||||
muteStates: MuteStates,
|
||||
mediaDevices: MediaDevices,
|
||||
openSettings: (() => void) | undefined,
|
||||
reactionIdentifier: string | undefined,
|
||||
): ViewModel<FooterSnapshot> {
|
||||
const { showControls, header: headerStyle } = getUrlParams();
|
||||
|
||||
const hideLogo = headerStyle !== HeaderStyle.Standard;
|
||||
|
||||
return {
|
||||
...buildMuteBehaviors(scope, muteStates),
|
||||
|
||||
// ── Visibility / sizing ──────────────────────────────────────────────────
|
||||
hideControls: constant(!showControls),
|
||||
asOverlay: scope.behavior(
|
||||
callModel.windowMode$.pipe(map((mode) => mode === "flat")),
|
||||
),
|
||||
buttonSize: scope.behavior(
|
||||
callModel.layout$.pipe(
|
||||
map((l) => (l.type === "pip" ? "md" : "lg") as "md" | "lg"),
|
||||
),
|
||||
),
|
||||
showSettingsButton: scope.behavior(
|
||||
combineLatest([callModel.layout$, callModel.showHeader$]).pipe(
|
||||
map(
|
||||
([l, showHeader]) =>
|
||||
openSettings !== undefined &&
|
||||
l.type !== "pip" &&
|
||||
showControls &&
|
||||
!(headerStyle === HeaderStyle.AppBar && showHeader),
|
||||
),
|
||||
),
|
||||
),
|
||||
showLayoutSwitcher: scope.behavior(
|
||||
callModel.layout$.pipe(map((l) => l.type !== "pip" && showControls)),
|
||||
),
|
||||
showLogoDebugContainer: scope.behavior(
|
||||
combineLatest([callModel.layout$, debugTileLayoutSetting.value$]).pipe(
|
||||
map(([l, debugTile]) => l.type !== "pip" || (!hideLogo && !debugTile)),
|
||||
),
|
||||
),
|
||||
showLogo: scope.behavior(
|
||||
callModel.layout$.pipe(map((l) => !hideLogo && l.type !== "pip")),
|
||||
),
|
||||
|
||||
// ── Layout mode ───────────────────────────────────────────────────────────
|
||||
layoutMode: callModel.gridMode$,
|
||||
setLayoutMode: constant(callModel.setGridMode),
|
||||
|
||||
// ── Screen sharing ────────────────────────────────────────────────────────
|
||||
sharingScreen: callModel.sharingScreen$,
|
||||
toggleScreenSharing: constant(callModel.toggleScreenSharing ?? undefined),
|
||||
|
||||
// ── Audio output ─────────────────────────────────────────────────────────
|
||||
audioOutputSwitcher: scope.behavior(
|
||||
callModel.audioOutputSwitcher$.pipe(
|
||||
map((switcher) => switcher ?? undefined),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────
|
||||
openSettings: scope.behavior(
|
||||
callModel.showHeader$.pipe(
|
||||
map((showHeader) =>
|
||||
headerStyle === HeaderStyle.AppBar && showHeader
|
||||
? undefined
|
||||
: openSettings,
|
||||
),
|
||||
),
|
||||
),
|
||||
hangup: constant(callModel.hangup),
|
||||
|
||||
// ── Reactions ─────────────────────────────────────────────────────────────
|
||||
reactionIdentifier: constant(reactionIdentifier),
|
||||
reactionData: constant(
|
||||
reactionIdentifier !== undefined
|
||||
? {
|
||||
handsRaised$: callModel.handsRaised$,
|
||||
reactions$: callModel.reactions$,
|
||||
}
|
||||
: undefined,
|
||||
),
|
||||
|
||||
// ── Debug ─────────────────────────────────────────────────────────────────
|
||||
debugTileLayout: debugTileLayoutSetting.value$,
|
||||
tileStoreGeneration: callModel.tileStoreGeneration$,
|
||||
|
||||
...buildDeviceBehaviors(scope, mediaDevices),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simplified ViewModel for the CallFooter used in the lobby
|
||||
* (pre-call) screen. Unlike createCallFooterViewModel, this does not require
|
||||
* a CallViewModel — it only needs mute states, device lists, and callbacks.
|
||||
*
|
||||
* @param scope - ObservableScope that bounds the lifetime of derived behaviors.
|
||||
* @param muteStates - Audio and video mute state + toggles.
|
||||
* @param mediaDevices - Available and selected input devices.
|
||||
* @param openSettings - Callback to open the settings modal, or undefined.
|
||||
* @param hangup - Callback to leave/cancel, or undefined (hides the button).
|
||||
* @param showLogo - Whether to show the Element Call logo.
|
||||
*/
|
||||
export function createLobbyFooterViewModel(
|
||||
scope: ObservableScope,
|
||||
muteStates: MuteStates,
|
||||
mediaDevices: MediaDevices,
|
||||
openSettings: (() => void) | undefined,
|
||||
hangup: (() => void) | undefined,
|
||||
showLogo: boolean,
|
||||
): ViewModel<FooterSnapshot> {
|
||||
return {
|
||||
...buildMuteBehaviors(scope, muteStates),
|
||||
...buildDeviceBehaviors(scope, mediaDevices),
|
||||
// ── Visibility / sizing ───────────────────────────────────────────────────
|
||||
hideControls: constant(false),
|
||||
asOverlay: constant(false),
|
||||
buttonSize: constant("lg"),
|
||||
showSettingsButton: constant(openSettings !== undefined),
|
||||
showLayoutSwitcher: constant(false),
|
||||
showLogoDebugContainer: constant(showLogo),
|
||||
showLogo: constant(showLogo),
|
||||
|
||||
// ── Layout mode (not applicable in lobby) ─────────────────────────────────
|
||||
layoutMode: constant(undefined),
|
||||
setLayoutMode: constant(undefined),
|
||||
|
||||
// ── Screen sharing (not applicable in lobby) ──────────────────────────────
|
||||
sharingScreen: constant(undefined),
|
||||
toggleScreenSharing: constant(undefined),
|
||||
|
||||
// ── Audio output (not applicable in lobby) ────────────────────────────────
|
||||
audioOutputSwitcher: constant(undefined),
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────
|
||||
openSettings: constant(openSettings),
|
||||
hangup: constant(hangup),
|
||||
|
||||
// ── Reactions (not applicable in lobby) ───────────────────────────────────
|
||||
reactionIdentifier: constant(undefined),
|
||||
reactionData: constant(undefined),
|
||||
|
||||
// ── Debug (not needed in lobby) ───────────────────────────────────────────
|
||||
debugTileLayout: constant(false),
|
||||
tileStoreGeneration: constant(0),
|
||||
};
|
||||
}
|
||||
|
||||
export interface FooterProps {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
children?: JSX.Element | JSX.Element[] | false;
|
||||
vm: ViewModel<FooterSnapshot>;
|
||||
}
|
||||
export const CallFooter: FC<FooterProps> = ({ ref, children, vm }) => {
|
||||
const {
|
||||
asOverlay,
|
||||
hideControls,
|
||||
layoutMode,
|
||||
setLayoutMode,
|
||||
openSettings,
|
||||
audioEnabled,
|
||||
videoEnabled,
|
||||
toggleAudio,
|
||||
toggleVideo,
|
||||
sharingScreen,
|
||||
toggleScreenSharing,
|
||||
reactionIdentifier,
|
||||
reactionData,
|
||||
audioOutputSwitcher,
|
||||
hangup,
|
||||
debugTileLayout,
|
||||
tileStoreGeneration,
|
||||
videoOptions,
|
||||
selectedVideo,
|
||||
audioOptions,
|
||||
selectedAudio,
|
||||
selectAudioButtonOption,
|
||||
selectVideoButtonOption,
|
||||
videoToggles,
|
||||
buttonSize,
|
||||
showSettingsButton,
|
||||
showLogoDebugContainer,
|
||||
showLogo,
|
||||
} = useViewModel(vm);
|
||||
|
||||
const buttons: JSX.Element[] = [];
|
||||
const buttonSize = asPip ? "md" : "lg";
|
||||
const showSettingsButton =
|
||||
openSettings !== undefined && !asPip && !hideControls;
|
||||
const showLayoutSwitcher = !asPip && !hideControls;
|
||||
const showLogoDebugContainer = !asPip || (!hideLogo && !debugTileLayout);
|
||||
const showLogo = !hideLogo && !asPip;
|
||||
|
||||
if (showSettingsButton) {
|
||||
// add the settings button to the center group of buttons, so it will be visible on small screens.
|
||||
// On larger screens, it will be hidden SettingsIconButton the one with `showForScreenWidth = "wide"` in the `settingsLogoContainer` will be visible.
|
||||
// Add the settings button to the center group so it's visible on small
|
||||
// screens. On larger screens the SettingsIconButton with
|
||||
// showForScreenWidth="wide" in the settingsLogoContainer is used instead.
|
||||
buttons.push(
|
||||
<SettingsButton
|
||||
key="settings"
|
||||
@@ -125,24 +417,60 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<MicButton
|
||||
size={buttonSize}
|
||||
key="audio"
|
||||
enabled={audioEnabled ?? false}
|
||||
onClick={toggleAudio}
|
||||
disabled={toggleAudio === undefined}
|
||||
data-testid="incall_mute"
|
||||
/>,
|
||||
<VideoButton
|
||||
size={buttonSize}
|
||||
key="video"
|
||||
enabled={videoEnabled ?? false}
|
||||
onClick={toggleVideo}
|
||||
disabled={toggleVideo === undefined}
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
);
|
||||
if ((audioOptions?.length ?? 0) > 0) {
|
||||
buttons.push(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Mic Source"}
|
||||
key="audio"
|
||||
iconsAndLabels="audio"
|
||||
enabled={audioEnabled ?? false}
|
||||
onMuteClick={toggleAudio}
|
||||
data-testid="incall_mute"
|
||||
options={audioOptions}
|
||||
selectedOption={selectedAudio}
|
||||
onSelect={selectAudioButtonOption}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
<MicButton
|
||||
size={buttonSize}
|
||||
key="audio"
|
||||
enabled={audioEnabled ?? false}
|
||||
onClick={toggleAudio}
|
||||
disabled={toggleAudio === undefined}
|
||||
data-testid="incall_mute"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if ((videoOptions?.length ?? 0) > 0) {
|
||||
buttons.push(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Camera Source"}
|
||||
key="video"
|
||||
iconsAndLabels="video"
|
||||
enabled={videoEnabled ?? false}
|
||||
onMuteClick={toggleVideo}
|
||||
data-testid="incall_videomute"
|
||||
options={videoOptions}
|
||||
toggles={videoToggles}
|
||||
selectedOption={selectedVideo}
|
||||
onSelect={selectVideoButtonOption}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
<VideoButton
|
||||
size={buttonSize}
|
||||
key="video"
|
||||
enabled={videoEnabled ?? false}
|
||||
onClick={toggleVideo}
|
||||
disabled={toggleVideo === undefined}
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (toggleScreenSharing !== undefined) {
|
||||
buttons.push(
|
||||
@@ -161,12 +489,7 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
buttons.push(
|
||||
<ReactionToggleButton
|
||||
size={buttonSize}
|
||||
reactionData={
|
||||
reactionData ?? {
|
||||
handsRaised$: new BehaviorSubject({}),
|
||||
reactions$: new BehaviorSubject({}),
|
||||
}
|
||||
}
|
||||
reactionData={reactionData}
|
||||
key="raise_hand"
|
||||
className={styles.raiseHand}
|
||||
identifier={reactionIdentifier}
|
||||
@@ -219,7 +542,6 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
ref={ref}
|
||||
className={classNames(styles.footer, {
|
||||
[styles.overlay]: asOverlay,
|
||||
[styles.hidden]: hidden,
|
||||
})}
|
||||
>
|
||||
<div className={styles.settingsLogoContainer}>
|
||||
@@ -236,7 +558,7 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
{showLogoDebugContainer && logoDebugContainer}
|
||||
</div>
|
||||
{!hideControls && <div className={styles.buttons}>{buttons}</div>}
|
||||
{setLayoutMode && layoutMode && showLayoutSwitcher && (
|
||||
{setLayoutMode && layoutMode && (
|
||||
<Switch<"spotlight", "grid">
|
||||
name="layoutMode"
|
||||
aria-label={t("layout_switch_label")}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
border-radius: 32px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
.containerOpen {
|
||||
background-color: var(--cpd-color-bg-action-primary-pressed);
|
||||
}
|
||||
.chevronIconOpen > svg {
|
||||
color: var(--cpd-color-icon-on-solid-primary);
|
||||
}
|
||||
.menuButton {
|
||||
width: 40px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.itemIcon {
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
.rotate {
|
||||
animation: spinner 1.5s linear infinite;
|
||||
}
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { AdvancedSettingsIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { fn, userEvent, within, expect } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton";
|
||||
|
||||
const meta = {
|
||||
component: MediaMuteAndSwitchButton,
|
||||
} satisfies Meta<typeof MediaMuteAndSwitchButton>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: "SomeMenu",
|
||||
iconsAndLabels: {
|
||||
IconEnabled: AdvancedSettingsIcon,
|
||||
IconDisabled: AdvancedSettingsIcon,
|
||||
enabledLabel: "Enabled",
|
||||
disabledLabel: "Disabled",
|
||||
optionsButtonLabel: "Options",
|
||||
},
|
||||
enabled: true,
|
||||
options: [
|
||||
{ label: "option 1", id: "1" },
|
||||
{ label: "option 2", id: "2" },
|
||||
],
|
||||
selectedOption: "1",
|
||||
onMuteClick: fn(),
|
||||
onSelect: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export const AudioMute: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
title: "Microphone",
|
||||
iconsAndLabels: "audio",
|
||||
enabled: false,
|
||||
options: [
|
||||
{ label: "Microphone 1", id: "1" },
|
||||
{ label: "Microphone 2", id: "2" },
|
||||
],
|
||||
toggles: [
|
||||
{
|
||||
label: "example toggle",
|
||||
id: "t0",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
selectedOption: "2",
|
||||
},
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
// Both the mute button and the chevron trigger currently share the aria-label "Edit"
|
||||
// (both are TODO placeholders in the component). The mute button is first in the DOM.
|
||||
const muteButton = canvas.getByLabelText("Unmute microphone");
|
||||
await userEvent.click(muteButton);
|
||||
await expect(args.onMuteClick).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const AudioUnmute: Story = {
|
||||
args: {
|
||||
title: "Microphone",
|
||||
iconsAndLabels: "audio",
|
||||
enabled: true,
|
||||
options: [
|
||||
{ label: "Microphone 1", id: "1" },
|
||||
{ label: "Microphone 2", id: "2" },
|
||||
],
|
||||
toggles: [],
|
||||
selectedOption: "2",
|
||||
},
|
||||
};
|
||||
|
||||
export const VideoMute: Story = {
|
||||
args: {
|
||||
title: "Camera",
|
||||
iconsAndLabels: "video",
|
||||
enabled: false,
|
||||
options: [
|
||||
{ label: "Camera 1", id: "1" },
|
||||
{ label: "Camera 2", id: "2" },
|
||||
],
|
||||
toggles: [],
|
||||
selectedOption: "1",
|
||||
},
|
||||
};
|
||||
|
||||
export const VideoUnmute: Story = {
|
||||
args: {
|
||||
title: "Camera",
|
||||
iconsAndLabels: "video",
|
||||
enabled: true,
|
||||
options: [
|
||||
{ label: "Camera 1", id: "1" },
|
||||
{ label: "Camera 2", id: "2" },
|
||||
],
|
||||
toggles: [
|
||||
{
|
||||
label: "background blurring",
|
||||
id: "background_blurring",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
selectedOption: "2",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
Copyright 2023, 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { act, render, screen, type RenderResult } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type JSX, useState } from "react";
|
||||
|
||||
import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton";
|
||||
|
||||
describe("MediaMuteAndSwitchButton", () => {
|
||||
test("renders", () => {
|
||||
const { container } = render(
|
||||
<MediaMuteAndSwitchButton title={"Switcher"} />,
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("renders correct audio and video labels", () => {
|
||||
const renderLabels = (
|
||||
type: "video" | "audio",
|
||||
enabled: boolean,
|
||||
): RenderResult => {
|
||||
return render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Switcher"}
|
||||
iconsAndLabels={type}
|
||||
enabled={enabled}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
const renderAudioEndabled = renderLabels("audio", true);
|
||||
const renderAudioDisabled = renderLabels("audio", false);
|
||||
const renderVideoEnabled = renderLabels("video", true);
|
||||
const renderVideoDisabled = renderLabels("video", false);
|
||||
|
||||
expect(
|
||||
renderAudioEndabled.getByRole("button", { name: "Mute microphone" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
renderAudioDisabled.getByRole("button", { name: "Unmute microphone" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
renderVideoEnabled.getByRole("button", { name: "Start video" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
renderVideoDisabled.getByRole("button", { name: "Stop video" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls mute on mute press", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onMute = vi.fn();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Switcher"}
|
||||
onMuteClick={onMute}
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Mute microphone" }));
|
||||
|
||||
expect(onMute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls select callback on menu click", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelect = vi.fn();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
options={[
|
||||
{ label: "Microphone 1", id: "mic1" },
|
||||
{ label: "Microphone 2", id: "mic2" },
|
||||
]}
|
||||
selectedOption="mic1"
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 2" }));
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith("mic2");
|
||||
});
|
||||
test("does not call select callback on already selected menu click", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelect = vi.fn();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
options={[
|
||||
{ label: "Microphone 1", id: "mic1" },
|
||||
{ label: "Microphone 2", id: "mic2" },
|
||||
]}
|
||||
selectedOption="mic1"
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 1" }));
|
||||
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders menu spinner until selection updates for the component", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
const onSelectPressed = vi.fn();
|
||||
const onOptionUpdated = vi.fn();
|
||||
function Wrapper(): JSX.Element {
|
||||
const [selectedOption, setSelectedOption] = useState("mic1");
|
||||
return (
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
options={[
|
||||
{ label: "Microphone 1", id: "mic1" },
|
||||
{ label: "Microphone 2", id: "mic2" },
|
||||
]}
|
||||
selectedOption={selectedOption}
|
||||
onSelect={(id) => {
|
||||
onSelectPressed();
|
||||
void promise.then(() => {
|
||||
setSelectedOption(id);
|
||||
onOptionUpdated();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { getByRole } = render(<Wrapper />);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Microphone 2" }));
|
||||
|
||||
expect(onSelectPressed).toHaveBeenCalled();
|
||||
expect(onOptionUpdated).not.toHaveBeenCalled();
|
||||
// After clicking, plannedSelection="mic2" but selectedOption is still "mic1",
|
||||
// so a spinner should appear on the mic2 item
|
||||
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
expect(mic2Item.querySelector(".rotate")).toBeTruthy();
|
||||
|
||||
// The currently-selected mic1 item should not have a spinner
|
||||
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
expect(mic1Item.querySelector(".rotate")).toBeNull();
|
||||
await act(async () => {
|
||||
// resolve the promise that acutally updates the select option.
|
||||
resolve();
|
||||
await promise;
|
||||
});
|
||||
|
||||
expect(onOptionUpdated).toHaveBeenCalled();
|
||||
// Spinner should now be gone since the selection has caught up
|
||||
const mic2ItemAfter = screen.getByRole("menuitem", {
|
||||
name: "Microphone 2",
|
||||
});
|
||||
expect(mic2ItemAfter.querySelector(".rotate")).toBeNull();
|
||||
});
|
||||
|
||||
test("renders menu with toggle control and calls toggle callback", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSelect = vi.fn();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
toggles={[{ label: "Background blur", id: "bg_blur", enabled: false }]}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
|
||||
const toggle = screen.getByRole("menuitemcheckbox", {
|
||||
name: "Background blur",
|
||||
});
|
||||
expect(toggle).toBeInTheDocument();
|
||||
expect(toggle).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
await user.click(toggle);
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith("bg_blur");
|
||||
});
|
||||
|
||||
test("renders check icon to mark the selected menu item", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { getByRole } = render(
|
||||
<MediaMuteAndSwitchButton
|
||||
title="Switcher"
|
||||
iconsAndLabels="audio"
|
||||
enabled={true}
|
||||
options={[
|
||||
{ label: "Microphone 1", id: "mic1" },
|
||||
{ label: "Microphone 2", id: "mic2" },
|
||||
]}
|
||||
selectedOption="mic2"
|
||||
/>,
|
||||
);
|
||||
|
||||
// open menu
|
||||
await user.click(getByRole("button", { name: "Microphone" }));
|
||||
|
||||
// The selected item (mic2) renders both an IconOptions SVG and a CheckIcon SVG
|
||||
const mic1Item = screen.getByRole("menuitem", { name: "Microphone 2" });
|
||||
expect(mic1Item.querySelectorAll("svg").length).toBe(2);
|
||||
|
||||
// The unselected item (mic1) only renders its IconOptions SVG
|
||||
const mic2Item = screen.getByRole("menuitem", { name: "Microphone 1" });
|
||||
expect(mic2Item.querySelectorAll("svg").length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ComponentType, useState, type FC } from "react";
|
||||
import {
|
||||
Button,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ToggleMenuItem,
|
||||
} from "@vector-im/compound-web";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronUpIcon,
|
||||
ChevronDownIcon,
|
||||
MicOffSolidIcon,
|
||||
MicOnIcon,
|
||||
MicOnSolidIcon,
|
||||
SpinnerIcon,
|
||||
VideoCallIcon,
|
||||
VideoCallOffSolidIcon,
|
||||
VideoCallSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import classNames from "classnames";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import styles from "./MediaMuteAndSwitchButton.module.css";
|
||||
|
||||
export interface MenuOptions {
|
||||
label: string;
|
||||
id: string;
|
||||
}
|
||||
export interface ToggleOption {
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface IconsAndLabels {
|
||||
/** The Icon used if the mute button is enabled */
|
||||
IconEnabled: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||
/** The Icon used if the mute button is disabled */
|
||||
IconDisabled: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||
/** The icon used for the different options */
|
||||
IconOptions?: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||
enabledLabel: string;
|
||||
disabledLabel: string;
|
||||
optionsButtonLabel: string;
|
||||
}
|
||||
|
||||
export interface MediaMuteAndSwitchButtonProps {
|
||||
/** The title used in the Switcher modal. */
|
||||
title: string;
|
||||
/** If the Mute button is enabled */
|
||||
enabled?: boolean;
|
||||
/** Callback if the mute button is clicked */
|
||||
onMuteClick?: () => void;
|
||||
iconsAndLabels?: "video" | "audio" | IconsAndLabels;
|
||||
/** The options available for the media device selector modal */
|
||||
options?: MenuOptions[];
|
||||
/** The option that will currently be rendered as the selected option */
|
||||
selectedOption?: string;
|
||||
/**
|
||||
* The available toggles (including there current state)
|
||||
* The toggle state is not stored by this component.
|
||||
* It is handled externally and needs to be set by listening to the `onSelect` callback and setting the right toggle item to `enabled`
|
||||
*/
|
||||
toggles?: ToggleOption[];
|
||||
/**
|
||||
* For any toggle and option this method will be called.
|
||||
* So toggles need to be implemented by listening here and setting the right toggle item to `enabled`
|
||||
*/
|
||||
onSelect?: (id: string) => void;
|
||||
}
|
||||
|
||||
export const MediaMuteAndSwitchButton: FC<MediaMuteAndSwitchButtonProps> = ({
|
||||
title,
|
||||
enabled,
|
||||
onMuteClick,
|
||||
iconsAndLabels: iconsAndLabelsWithDefaultCases,
|
||||
options,
|
||||
selectedOption,
|
||||
toggles,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [plannedSelection, setPlannedSelection] = useState<string | null>(null);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
let iconsAndLabels: IconsAndLabels | undefined;
|
||||
switch (iconsAndLabelsWithDefaultCases) {
|
||||
case "video":
|
||||
iconsAndLabels = {
|
||||
IconEnabled: VideoCallSolidIcon,
|
||||
IconDisabled: VideoCallOffSolidIcon,
|
||||
IconOptions: VideoCallIcon,
|
||||
disabledLabel: t("stop_video_button_label"),
|
||||
enabledLabel: t("start_video_button_label"),
|
||||
optionsButtonLabel: t("settings.devices.microphone"),
|
||||
};
|
||||
break;
|
||||
case "audio":
|
||||
iconsAndLabels = {
|
||||
IconEnabled: MicOnSolidIcon,
|
||||
IconDisabled: MicOffSolidIcon,
|
||||
IconOptions: MicOnIcon,
|
||||
disabledLabel: t("mute_microphone_button_label"),
|
||||
enabledLabel: t("unmute_microphone_button_label"),
|
||||
optionsButtonLabel: t("settings.devices.microphone"),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
iconsAndLabels = iconsAndLabelsWithDefaultCases;
|
||||
break;
|
||||
}
|
||||
const {
|
||||
IconEnabled,
|
||||
IconDisabled,
|
||||
IconOptions,
|
||||
disabledLabel,
|
||||
enabledLabel,
|
||||
optionsButtonLabel,
|
||||
} = iconsAndLabels ?? {
|
||||
IconEnabled: undefined,
|
||||
IconDisabled: undefined,
|
||||
IconOptions: undefined,
|
||||
disabledLabel: undefined,
|
||||
enabledLabel: undefined,
|
||||
optionsButtonLabel: undefined,
|
||||
};
|
||||
{
|
||||
logger.info(
|
||||
"RENDER WITH: selectedOption !== option.id && plannedSelection === option.id",
|
||||
selectedOption,
|
||||
" !==",
|
||||
"option.id",
|
||||
" && ",
|
||||
plannedSelection,
|
||||
" === ",
|
||||
"option.id",
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
[styles.container]: true,
|
||||
[styles.containerOpen]: menuOpen,
|
||||
})}
|
||||
>
|
||||
{/* The mute button lives inside */}
|
||||
<Button
|
||||
iconOnly
|
||||
Icon={enabled ? IconEnabled : IconDisabled}
|
||||
onClick={(e) => {
|
||||
onMuteClick?.();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
kind={enabled ? "secondary" : "primary"}
|
||||
size="lg"
|
||||
className={styles.button}
|
||||
aria-label={enabled ? disabledLabel : enabledLabel}
|
||||
/>
|
||||
<Menu
|
||||
title={title}
|
||||
showTitle={true}
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
side="top"
|
||||
trigger={
|
||||
<Button
|
||||
iconOnly
|
||||
className={classNames({
|
||||
[styles.menuButton]: true,
|
||||
[styles.chevronIconOpen]: menuOpen,
|
||||
})}
|
||||
Icon={menuOpen ? ChevronUpIcon : ChevronDownIcon}
|
||||
kind={"tertiary"}
|
||||
size="lg"
|
||||
aria-label={optionsButtonLabel}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{options?.map((option) => (
|
||||
<MenuItem
|
||||
hideChevron
|
||||
label={option.label}
|
||||
Icon={
|
||||
IconOptions && (
|
||||
<IconOptions
|
||||
width={24}
|
||||
height={24}
|
||||
className={styles.itemIcon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
if (option.id === selectedOption) return;
|
||||
setPlannedSelection(option.id);
|
||||
onSelect?.(option.id);
|
||||
}}
|
||||
key={option.id}
|
||||
>
|
||||
{selectedOption === option.id && (
|
||||
<CheckIcon width={24} height={24} />
|
||||
)}
|
||||
{selectedOption !== option.id && plannedSelection === option.id && (
|
||||
<SpinnerIcon width={24} height={24} className={styles.rotate} />
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
{(toggles?.length ?? 0) > 0 && <hr />}
|
||||
{toggles?.map((toggle) => (
|
||||
<ToggleMenuItem
|
||||
label={toggle.label}
|
||||
onSelect={(e) => {
|
||||
onSelect?.(toggle.id);
|
||||
e.preventDefault();
|
||||
}}
|
||||
checked={toggle.enabled}
|
||||
key={toggle.id}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MediaMuteAndSwitchButton > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<button
|
||||
class="_button_1nw83_8 button _icon-only_1nw83_53"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
class="_button_1nw83_8 menuButton _has-icon_1nw83_60 _icon-only_1nw83_53"
|
||||
data-kind="tertiary"
|
||||
data-size="lg"
|
||||
data-state="closed"
|
||||
id="radix-_r_0_"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
+35
-48
@@ -43,7 +43,6 @@ import { InviteButton } from "../button/InviteButton";
|
||||
import {
|
||||
type CallViewModel,
|
||||
createCallViewModel$,
|
||||
type GridMode,
|
||||
} from "../state/CallViewModel/CallViewModel.ts";
|
||||
import { Grid, type TileProps } from "../grid/Grid";
|
||||
import { useInitial } from "../useInitial";
|
||||
@@ -68,11 +67,7 @@ import {
|
||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||
import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
|
||||
import {
|
||||
debugTileLayout as debugTileLayoutSetting,
|
||||
matrixRTCMode as matrixRTCModeSetting,
|
||||
useSetting,
|
||||
} from "../settings/settings";
|
||||
import { matrixRTCMode as matrixRTCModeSetting } from "../settings/settings";
|
||||
import { ReactionsReader } from "../reactions/ReactionsReader";
|
||||
import { LivekitRoomAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx";
|
||||
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
|
||||
@@ -90,7 +85,10 @@ import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.t
|
||||
import { type Layout } from "../state/layout-types.ts";
|
||||
import { ObservableScope } from "../state/ObservableScope.ts";
|
||||
import { useLatest } from "../useLatest.ts";
|
||||
import { CallFooter } from "../components/CallFooter.tsx";
|
||||
import {
|
||||
CallFooter,
|
||||
createCallFooterViewModel,
|
||||
} from "../components/CallFooter.tsx";
|
||||
import { SettingsIconButton } from "../button/Button.tsx";
|
||||
|
||||
const logger = rootLogger.getChild("[InCallView]");
|
||||
@@ -220,9 +218,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
muted: muteAllAudio,
|
||||
});
|
||||
const latestPickupPhaseAudio = useLatest(pickupPhaseAudio);
|
||||
|
||||
const audioEnabled = useBehavior(muteStates.audio.enabled$);
|
||||
const videoEnabled = useBehavior(muteStates.video.enabled$);
|
||||
const mediaDevices = useMediaDevices();
|
||||
const toggleAudio = useBehavior(muteStates.audio.toggle$);
|
||||
const toggleVideo = useBehavior(muteStates.video.toggle$);
|
||||
const setAudioEnabled = useBehavior(muteStates.audio.setEnabled$);
|
||||
@@ -241,14 +237,10 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const reconnecting = useBehavior(vm.reconnecting$);
|
||||
const windowMode = useBehavior(vm.windowMode$);
|
||||
const layout = useBehavior(vm.layout$);
|
||||
const tileStoreGeneration = useBehavior(vm.tileStoreGeneration$);
|
||||
const [debugTileLayout] = useSetting(debugTileLayoutSetting);
|
||||
const gridMode = useBehavior(vm.gridMode$);
|
||||
const showHeader = useBehavior(vm.showHeader$);
|
||||
const showFooter = useBehavior(vm.showFooter$);
|
||||
const earpieceMode = useBehavior(vm.earpieceMode$);
|
||||
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
||||
const sharingScreen = useBehavior(vm.sharingScreen$);
|
||||
|
||||
const fatalCallError = useBehavior(vm.fatalError$);
|
||||
// Stop the rendering and throw for the error boundary
|
||||
@@ -348,11 +340,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
() => new BehaviorSubject(defaultPipAlignment),
|
||||
);
|
||||
|
||||
const setGridMode = useCallback(
|
||||
(mode: GridMode) => vm.setGridMode(mode),
|
||||
[vm],
|
||||
);
|
||||
|
||||
useAppBarHidden(!showHeader);
|
||||
|
||||
let header: ReactNode = null;
|
||||
@@ -559,8 +546,34 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
matrixRoom.roomId,
|
||||
);
|
||||
|
||||
const settingsButtonInAppBar =
|
||||
headerStyle === HeaderStyle.AppBar && showHeader;
|
||||
const footerScope = useMemo(() => new ObservableScope(), []);
|
||||
useEffect(() => (): void => footerScope.end(), [footerScope]);
|
||||
// Build the footer view-model once per stable set of domain-object references.
|
||||
// The scalar inputs (reactionIdentifier) are derived from URL params and are
|
||||
// effectively static for the call lifetime.
|
||||
const footerVm = useMemo(
|
||||
() =>
|
||||
createCallFooterViewModel(
|
||||
footerScope,
|
||||
vm,
|
||||
muteStates,
|
||||
mediaDevices,
|
||||
openSettings,
|
||||
supportsReactions
|
||||
? `${client.getUserId()}:${client.getDeviceId()}`
|
||||
: undefined,
|
||||
),
|
||||
[
|
||||
footerScope,
|
||||
vm,
|
||||
muteStates,
|
||||
mediaDevices,
|
||||
openSettings,
|
||||
supportsReactions,
|
||||
client,
|
||||
],
|
||||
);
|
||||
|
||||
useAppBarSecondaryButton(
|
||||
<SettingsIconButton
|
||||
key="settings"
|
||||
@@ -571,33 +584,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
|
||||
// Only hide the settings button if we have an AppBar header and we are showing the header
|
||||
const footer = (
|
||||
<CallFooter
|
||||
ref={footerRef}
|
||||
hidden={!showFooter}
|
||||
hideControls={!showControls}
|
||||
asOverlay={windowMode === "flat"}
|
||||
asPip={layout.type === "pip"}
|
||||
// Hide the logo for both embedded solutions. mobile: HeaderStyle.AppBar and desktop: HeaderStyle.None.
|
||||
hideLogo={headerStyle !== HeaderStyle.Standard}
|
||||
layoutMode={gridMode}
|
||||
setLayoutMode={setGridMode}
|
||||
audioEnabled={audioEnabled}
|
||||
toggleAudio={toggleAudio ?? undefined}
|
||||
videoEnabled={videoEnabled}
|
||||
toggleVideo={toggleVideo ?? undefined}
|
||||
sharingScreen={sharingScreen}
|
||||
toggleScreenSharing={vm.toggleScreenSharing ?? undefined}
|
||||
reactionIdentifier={`${client.getUserId()}:${client.getDeviceId()}`}
|
||||
reactionData={supportsReactions ? vm : undefined}
|
||||
audioOutputSwitcher={audioOutputSwitcher ?? undefined}
|
||||
// Only pass the openSettings function if the settings button is not in the app bar.
|
||||
// If there is no fn the button will be hidden in the footer.
|
||||
openSettings={settingsButtonInAppBar ? undefined : openSettings}
|
||||
hangup={vm.hangup}
|
||||
//Debug props
|
||||
debugTileLayout={debugTileLayout}
|
||||
tileStoreGeneration={tileStoreGeneration}
|
||||
/>
|
||||
<>{showFooter && <CallFooter ref={footerRef} vm={footerVm} />}</>
|
||||
);
|
||||
const allConnections = useBehavior(vm.allConnections$);
|
||||
|
||||
|
||||
+21
-11
@@ -38,6 +38,7 @@ import { useMediaQuery } from "../useMediaQuery";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { Link } from "../button/Link";
|
||||
import { useMediaDevices } from "../MediaDevicesContext";
|
||||
import { ObservableScope } from "../state/ObservableScope";
|
||||
import { useInitial } from "../useInitial";
|
||||
import {
|
||||
useTrackProcessor,
|
||||
@@ -46,7 +47,10 @@ import {
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
import { getValue } from "../utils/observable";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
import { CallFooter } from "../components/CallFooter";
|
||||
import {
|
||||
CallFooter,
|
||||
createLobbyFooterViewModel,
|
||||
} from "../components/CallFooter";
|
||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||
|
||||
interface Props {
|
||||
@@ -184,6 +188,21 @@ export const LobbyView: FC<Props> = ({
|
||||
|
||||
useTrackProcessorSync(videoTrack);
|
||||
|
||||
const footerScope = useInitial(() => new ObservableScope());
|
||||
useEffect((): (() => void) => () => footerScope.end(), [footerScope]);
|
||||
|
||||
const footerVm = useInitial(() =>
|
||||
createLobbyFooterViewModel(
|
||||
footerScope,
|
||||
muteStates,
|
||||
devices,
|
||||
openSettings,
|
||||
!confineToRoom ? onLeaveClick : undefined,
|
||||
// Logo and header are connected: only show the logo in SPA with header.
|
||||
!hideHeader,
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: Unify this component with InCallView, so we can get slick joining
|
||||
// animations and don't have to feel bad about reusing its CSS
|
||||
return (
|
||||
@@ -227,16 +246,7 @@ export const LobbyView: FC<Props> = ({
|
||||
</VideoPreview>
|
||||
{!recentsButtonInFooter && recentsButton}
|
||||
</div>
|
||||
<CallFooter
|
||||
audioEnabled={audioEnabled}
|
||||
videoEnabled={videoEnabled}
|
||||
toggleAudio={toggleAudio ?? undefined}
|
||||
toggleVideo={toggleVideo ?? undefined}
|
||||
openSettings={openSettings}
|
||||
hangup={!confineToRoom ? onLeaveClick : undefined}
|
||||
// Logo and header are connected. We will only show the logo in SPA with header.
|
||||
hideLogo={hideHeader}
|
||||
>
|
||||
<CallFooter vm={footerVm}>
|
||||
{recentsButtonInFooter && recentsButton}
|
||||
</CallFooter>
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type FC,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useId,
|
||||
} from "react";
|
||||
import { type ChangeEvent, type FC, useCallback, useId } from "react";
|
||||
import {
|
||||
Heading,
|
||||
InlineField,
|
||||
@@ -20,8 +13,8 @@ import {
|
||||
RadioControl,
|
||||
Separator,
|
||||
} from "@vector-im/compound-web";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import { t } from "i18next";
|
||||
|
||||
import {
|
||||
type AudioOutputDeviceLabel,
|
||||
@@ -37,12 +30,39 @@ interface Props {
|
||||
numberedLabel: (number: number) => string;
|
||||
}
|
||||
|
||||
export function mediaDeviceLabelToString(
|
||||
label: DeviceLabel | AudioOutputDeviceLabel,
|
||||
numberedLabel: (number: number) => string,
|
||||
): string {
|
||||
let labelText = "";
|
||||
switch (label.type) {
|
||||
case "name":
|
||||
labelText = label.name;
|
||||
break;
|
||||
case "number":
|
||||
labelText = numberedLabel(label.number);
|
||||
break;
|
||||
case "default":
|
||||
labelText =
|
||||
label.name === null
|
||||
? t("settings.devices.default")
|
||||
: t("settings.devices.default") + " (" + label.name + ")";
|
||||
break;
|
||||
case "speaker":
|
||||
labelText = t("settings.devices.loudspeaker");
|
||||
break;
|
||||
case "earpiece":
|
||||
labelText = t("settings.devices.handset");
|
||||
break;
|
||||
}
|
||||
return labelText;
|
||||
}
|
||||
|
||||
export const DeviceSelection: FC<Props> = ({
|
||||
device,
|
||||
title,
|
||||
numberedLabel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const groupId = useId();
|
||||
const available = useObservableEagerState(device.available$);
|
||||
const selectedId = useObservableEagerState(device.selected$)?.id;
|
||||
@@ -70,38 +90,7 @@ export const DeviceSelection: FC<Props> = ({
|
||||
<Separator className={styles.separator} />
|
||||
<div className={styles.options}>
|
||||
{[...available].map(([id, label]) => {
|
||||
let labelText: ReactNode;
|
||||
switch (label.type) {
|
||||
case "name":
|
||||
labelText = label.name;
|
||||
break;
|
||||
case "number":
|
||||
labelText = numberedLabel(label.number);
|
||||
break;
|
||||
case "default":
|
||||
labelText =
|
||||
label.name === null ? (
|
||||
t("settings.devices.default")
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="settings.devices.default_named"
|
||||
name={label.name}
|
||||
>
|
||||
Default{" "}
|
||||
<span className={styles.secondary}>
|
||||
({{ name: label.name } as unknown as ReactElement})
|
||||
</span>
|
||||
</Trans>
|
||||
);
|
||||
break;
|
||||
case "speaker":
|
||||
labelText = t("settings.devices.loudspeaker");
|
||||
break;
|
||||
case "earpiece":
|
||||
labelText = t("settings.devices.handset");
|
||||
break;
|
||||
}
|
||||
|
||||
const labelText = mediaDeviceLabelToString(label, numberedLabel);
|
||||
return (
|
||||
<InlineField
|
||||
key={id}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2026 Element Software Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { useBehavior } from "../useBehavior";
|
||||
import { type Behavior } from "./Behavior";
|
||||
|
||||
export type ViewModel<Snapshot> = {
|
||||
[K in keyof Snapshot]: Behavior<Snapshot[K]>;
|
||||
};
|
||||
|
||||
export function useViewModel<Snapshot>(vm: ViewModel<Snapshot>): Snapshot {
|
||||
const snapshot = {} as Snapshot;
|
||||
for (const key in vm) {
|
||||
const value$ = vm[key];
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
snapshot[key] = useBehavior(value$);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export function createMockedViewModel<Snapshot>(
|
||||
snapshot: Snapshot,
|
||||
): ViewModel<Snapshot> {
|
||||
const vm = {} as ViewModel<Snapshot>;
|
||||
for (const key in snapshot) {
|
||||
vm[key] = new BehaviorSubject(snapshot[key]);
|
||||
}
|
||||
return vm;
|
||||
}
|
||||
+18
-12
@@ -24,18 +24,10 @@ import react from "@vitejs/plugin-react";
|
||||
import { realpathSync } from "fs";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
// Modified type helper from defineConfig to allow for packageType (see defineConfig from vite)
|
||||
export default ({
|
||||
export const vitePluginsConfig = ({
|
||||
mode,
|
||||
packageType,
|
||||
}: ConfigEnv & { packageType?: "full" | "embedded" }): UserConfig => {
|
||||
}: Pick<ConfigEnv, "mode">): UserConfig => {
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
// Environment variables with the VITE_ prefix are accessible at runtime.
|
||||
// So, we set this to allow for build/package specific behavior.
|
||||
// In future we might be able to do what is needed via code splitting at
|
||||
// build time.
|
||||
process.env.VITE_PACKAGE = packageType ?? "full";
|
||||
const plugins: PluginOption[] = [
|
||||
react(),
|
||||
wasm(),
|
||||
@@ -72,7 +64,7 @@ export default ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env.STORYBOOK) {
|
||||
if (!process.env.STORYBOOK && !process.env.VITEST) {
|
||||
plugins.push(
|
||||
createHtmlPlugin({
|
||||
entry: "src/main.tsx",
|
||||
@@ -86,6 +78,20 @@ export default ({
|
||||
);
|
||||
}
|
||||
|
||||
return { plugins };
|
||||
};
|
||||
// https://vitejs.dev/config/
|
||||
// Modified type helper from defineConfig to allow for packageType (see defineConfig from vite)
|
||||
export default ({
|
||||
mode,
|
||||
packageType,
|
||||
}: ConfigEnv & { packageType?: "full" | "embedded" }): UserConfig => {
|
||||
// Environment variables with the VITE_ prefix are accessible at runtime.
|
||||
// So, we set this to allow for build/package specific behavior.
|
||||
// In future we might be able to do what is needed via code splitting at
|
||||
// build time.
|
||||
process.env.VITE_PACKAGE = packageType ?? "full";
|
||||
|
||||
// The crypto WASM module is imported dynamically. Since it's common
|
||||
// for developers to use a linked copy of matrix-js-sdk or Rust
|
||||
// crypto (which could reside anywhere on their file system), Vite
|
||||
@@ -102,6 +108,7 @@ export default ({
|
||||
console.log("Allowed vite paths:", allow);
|
||||
|
||||
return {
|
||||
...vitePluginsConfig({ mode }),
|
||||
server: {
|
||||
port: 3000,
|
||||
fs: { allow },
|
||||
@@ -136,7 +143,6 @@ export default ({
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins,
|
||||
resolve: {
|
||||
alias: {
|
||||
// matrix-widget-api has its transpiled lib/index.js as its entry point,
|
||||
|
||||
+43
-9
@@ -1,20 +1,54 @@
|
||||
import { defineConfig, mergeConfig } from "vitest/config";
|
||||
import { playwright } from "@vitest/browser-playwright";
|
||||
import { vitePluginsConfig } from "./vite.config";
|
||||
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import viteConfig from "./vite.config";
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig((configEnv) =>
|
||||
mergeConfig(
|
||||
viteConfig(configEnv),
|
||||
vitePluginsConfig(configEnv),
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
css: {
|
||||
modules: {
|
||||
classNameStrategy: "non-scoped",
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
css: {
|
||||
modules: {
|
||||
classNameStrategy: "non-scoped",
|
||||
},
|
||||
},
|
||||
setupFiles: ["src/vitest.setup.ts"],
|
||||
environment: "jsdom",
|
||||
// an example of file based convention,
|
||||
// you don't have to follow it
|
||||
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||
name: "unit",
|
||||
},
|
||||
},
|
||||
},
|
||||
setupFiles: ["src/vitest.setup.ts"],
|
||||
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||
{
|
||||
plugins: [
|
||||
storybookTest({
|
||||
// The location of your Storybook config, main.js|ts
|
||||
configDir: path.join(dirname, ".storybook"),
|
||||
}),
|
||||
...vitePluginsConfig(configEnv).plugins!,
|
||||
],
|
||||
test: {
|
||||
name: "storybook",
|
||||
browser: {
|
||||
enabled: true,
|
||||
// Make sure to install Playwright
|
||||
provider: playwright(),
|
||||
headless: true,
|
||||
instances: [{ browser: "chromium" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
coverage: {
|
||||
reporter: ["html", "json"],
|
||||
include: ["src/**/*.{ts,tsx,js,jsx}"],
|
||||
|
||||
Reference in New Issue
Block a user