Merge branch 'livekit' into toger5/move-settings-out-of-bottom-bar

This commit is contained in:
Timo K
2026-04-13 18:36:03 +02:00
14 changed files with 370 additions and 103 deletions
+2 -2
View File
@@ -47,8 +47,8 @@ possible to support encryption.
These parameters are relevant to both [widget](./embedded-standalone.md) and [standalone](./embedded-standalone.md) modes:
| Name | Values | Required for widget | Required for SPA | Description |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `intent` | `start_call`, `join_existing`, `start_call_dm`, `join_existing_dm. | No, defaults to `start_call` | No, defaults to `start_call` | The intent is a special url parameter that defines the defaults for all the other parameters. In most cases it should be enough to only set the intent to setup element-call. |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `intent` | `start_call`, `join_existing`, `start_call_voice`, `join_existing_voice`, `start_call_dm`, `join_existing_dm`, `start_call_dm_voice`, or `join_existing_dm_voice`. | No, defaults to `start_call` | No, defaults to `start_call` | The intent is a special url parameter that defines the defaults for all the other parameters. In most cases it should be enough to only set the intent to setup element-call. |
| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesnt provide any. |
| `posthogUserId` | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. |
| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. |
+2 -2
View File
@@ -80,8 +80,8 @@
"@typescript-eslint/eslint-plugin": "^8.31.0",
"@typescript-eslint/parser": "^8.31.0",
"@use-gesture/react": "^10.2.11",
"@vector-im/compound-design-tokens": "^9.0.0",
"@vector-im/compound-web": "^8.0.0",
"@vector-im/compound-design-tokens": "^10.0.0",
"@vector-im/compound-web": "^9.0.0",
"@vitejs/plugin-react": "^4.0.1",
"@vitest/coverage-v8": "^4.0.18",
"babel-plugin-transform-vite-meta-env": "^1.0.3",
@@ -0,0 +1,92 @@
/*
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 { expect, test } from "@playwright/test";
import { widgetTest } from "../fixtures/widget-user";
import { HOST1, HOST2, TestHelpers } from "./test-helpers";
widgetTest(
"Bug new joiner was not publishing on correct SFU",
async ({ addUser, browserName }) => {
test.skip(
browserName === "firefox",
"This is a bug in the old widget, not a browser problem.",
);
test.slow();
// 2 users in federation
const florian = await addUser("floriant", HOST1);
const timo = await addUser("timo", HOST2);
// Florian creates a room and invites Timo to it
const roomName = "Call Room";
await TestHelpers.createRoom(roomName, florian.page, [timo.mxId]);
// Timo joins the room
await TestHelpers.acceptRoomInvite(roomName, timo.page);
// Ensure we are in legacy mode (should be the default)
await TestHelpers.openWidgetSetEmbeddedElementCallRtcModeCloseWidget(
florian.page,
"legacy",
);
await TestHelpers.openWidgetSetEmbeddedElementCallRtcModeCloseWidget(
timo.page,
"legacy",
);
// Let timo create a call
await TestHelpers.startCallInCurrentRoom(timo.page, false);
await TestHelpers.joinCallFromLobby(timo.page);
// We want to simulate that the oldest membership authentication is way slower than
// the preffered auth.
// In this setup, timo advertised$ transport will be it's own, and the active will be the one from florian
await florian.page.route(
"**/matrix-rtc.othersite.m.localhost/livekit/jwt/**",
async (route) => {
await new Promise((resolve) => setTimeout(resolve, 2000)); // 5 second delay
await route.continue();
},
);
// Florian joins the call
await expect(florian.page.getByTestId("join-call-button")).toBeVisible();
await florian.page.getByTestId("join-call-button").click();
await TestHelpers.joinCallFromLobby(florian.page);
await florian.page.waitForTimeout(3000);
await timo.page.waitForTimeout(3000);
// We should see 2 video tiles everywhere now
for (const user of [timo, florian]) {
const frame = user.page
.locator('iframe[title="Element Call"]')
.contentFrame();
await expect(frame.getByTestId("videoTile")).toHaveCount(2);
// No one should be waiting for media
await expect(frame.getByText("Waiting for media...")).not.toBeVisible();
// There should be 2 video elements, visible and autoplaying
const videoElements = await frame.locator("video").all();
expect(videoElements.length).toBe(2);
const blockDisplayCount = await frame
.locator("video")
.evaluateAll(
(videos: Element[]) =>
videos.filter(
(v: Element) => window.getComputedStyle(v).display === "block",
).length,
);
expect(blockDisplayCount).toBe(2);
}
},
);
+28 -1
View File
@@ -271,7 +271,11 @@ describe("UrlParams", () => {
computeUrlParams(
"?intent=start_call&widgetId=1234&parentUrl=parent.org",
),
).toMatchObject({ ...startNewCallDefaults("desktop"), skipLobby: false });
).toMatchObject({
...startNewCallDefaults("desktop"),
skipLobby: false,
callIntent: "video",
});
});
it("accepts start_call_dm mobile", () => {
@@ -308,6 +312,29 @@ describe("UrlParams", () => {
),
).toMatchObject(joinExistingCallDefaults("desktop"));
});
it("accepts start_call_voice", () => {
expect(
computeUrlParams(
"?intent=start_call_voice&widgetId=1234&parentUrl=parent.org",
),
).toMatchObject({
...startNewCallDefaults("desktop"),
skipLobby: false,
callIntent: "audio",
});
});
it("accepts join_existing_voice", () => {
expect(
computeUrlParams(
"?intent=join_existing_voice&widgetId=1234&parentUrl=parent.org",
),
).toMatchObject({
...joinExistingCallDefaults("desktop"),
callIntent: "audio",
});
});
});
describe("skipLobby", () => {
+11
View File
@@ -29,6 +29,8 @@ interface RoomIdentifier {
export enum UserIntent {
StartNewCall = "start_call",
JoinExistingCall = "join_existing",
StartNewCallVoice = "start_call_voice",
JoinExistingCallVoice = "join_existing_voice",
StartNewCallDM = "start_call_dm",
StartNewCallDMVoice = "start_call_dm_voice",
JoinExistingCallDM = "join_existing_dm",
@@ -414,6 +416,15 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
intentPreset.skipLobby = false;
intentPreset.callIntent = "video";
break;
case UserIntent.StartNewCallVoice:
intentPreset.skipLobby = false;
intentPreset.callIntent = "audio";
break;
case UserIntent.JoinExistingCallVoice:
// On desktop this will be overridden based on which button was used to join the call
intentPreset.skipLobby = false;
intentPreset.callIntent = "audio";
break;
case UserIntent.StartNewCallDMVoice:
intentPreset.callIntent = "audio";
// Fall through
+7 -3
View File
@@ -53,9 +53,8 @@ Please see LICENSE in the repository root for full details.
}
.footer.overlay {
position: absolute;
inset-block-end: 0;
inset-inline: 0;
/* 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. */
opacity: 1;
transition: opacity 0.15s;
}
@@ -64,6 +63,11 @@ Please see LICENSE in the repository root for full details.
display: grid;
opacity: 0;
pointer-events: none;
/* Switch to position: absolute so the footer takes up no space in the layout
when hidden. */
position: absolute;
inset-block-end: 0;
inset-inline: 0;
}
.footer.overlay:has(:focus-visible) {
@@ -108,8 +108,9 @@ exports[`ConnectionLostError: Action handling should reset error state 1`] = `
class="error"
>
<div
class="_content_1r8kr_8 icon"
data-size="large"
class="_big-icon_1ssbv_8 icon"
data-kind="primary"
data-size="lg"
>
<svg
aria-hidden="true"
@@ -265,8 +266,9 @@ exports[`should have a close button in widget mode 1`] = `
class="error"
>
<div
class="_content_1r8kr_8 icon"
data-size="large"
class="_big-icon_1ssbv_8 icon"
data-kind="primary"
data-size="lg"
>
<svg
aria-hidden="true"
@@ -418,8 +420,9 @@ exports[`should render the error page with link back to home 1`] = `
class="error"
>
<div
class="_content_1r8kr_8 icon"
data-size="large"
class="_big-icon_1ssbv_8 icon"
data-kind="primary"
data-size="lg"
>
<svg
aria-hidden="true"
@@ -571,8 +574,9 @@ exports[`should report correct error for 'Call is not supported' 1`] = `
class="error"
>
<div
class="_content_1r8kr_8 icon"
data-size="large"
class="_big-icon_1ssbv_8 icon"
data-kind="primary"
data-size="lg"
>
<svg
aria-hidden="true"
@@ -724,8 +728,9 @@ exports[`should report correct error for 'Connection lost' 1`] = `
class="error"
>
<div
class="_content_1r8kr_8 icon"
data-size="large"
class="_big-icon_1ssbv_8 icon"
data-kind="primary"
data-size="lg"
>
<svg
aria-hidden="true"
@@ -881,8 +886,9 @@ exports[`should report correct error for 'Incompatible browser' 1`] = `
class="error"
>
<div
class="_content_1r8kr_8 icon"
data-size="large"
class="_big-icon_1ssbv_8 icon"
data-kind="primary"
data-size="lg"
>
<svg
aria-hidden="true"
@@ -1029,8 +1035,9 @@ exports[`should report correct error for 'Insufficient capacity' 1`] = `
class="error"
>
<div
class="_content_1r8kr_8 icon"
data-size="large"
class="_big-icon_1ssbv_8 icon"
data-kind="primary"
data-size="lg"
>
<svg
aria-hidden="true"
@@ -17,7 +17,7 @@ exports[`InCallView > rendering > renders 1`] = `
>
<span
aria-label=""
class="_avatar_zysgz_8 roomAvatar _avatar-imageless_zysgz_55"
class="_avatar_7h2br_8 roomAvatar _avatar-imageless_7h2br_55"
data-color="1"
data-type="round"
role="img"
@@ -117,8 +117,9 @@ exports[`InCallView > rendering > renders 1`] = `
data-show="false"
>
<div
class="_content_1r8kr_8 icon"
data-size="large"
class="_big-icon_1ssbv_8 icon"
data-kind="primary"
data-size="lg"
>
<svg
aria-hidden="true"
+1 -3
View File
@@ -546,9 +546,7 @@ export function createCallViewModel$(
},
connectionManager,
matrixRTCSession,
localTransport$: scope.behavior(
localTransport$.pipe(switchMap((t) => t.advertised$)),
),
localTransport$,
logger: logger.getChild(`[${Date.now()}]`),
});
@@ -33,13 +33,20 @@ import {
PublishState,
TrackState,
} from "./LocalMember";
import { MatrixRTCTransportMissingError } from "../../../utils/errors";
import {
FailToGetOpenIdToken,
MatrixRTCTransportMissingError,
} from "../../../utils/errors";
import { Epoch, ObservableScope } from "../../ObservableScope";
import { constant } from "../../Behavior";
import { ConnectionManagerData } from "../remoteMembers/ConnectionManager";
import { ConnectionState, type Connection } from "../remoteMembers/Connection";
import { type Publisher } from "./Publisher";
import { initializeWidget } from "../../../widget";
import {
type LocalTransport,
type LocalTransportWithSFUConfig,
} from "./LocalTransport";
initializeWidget();
@@ -214,7 +221,7 @@ describe("LocalMembership", () => {
};
it("throws error on missing RTC config error", () => {
withTestScheduler(({ scope, hot, expectObservable }) => {
withTestScheduler(({ scope, hot, behavior, expectObservable }) => {
const localTransport$ = scope.behavior<null | LivekitTransportConfig>(
hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")),
null,
@@ -230,11 +237,16 @@ describe("LocalMembership", () => {
),
};
const aLocalTransport: LocalTransport = {
advertised$: localTransport$,
active$: behavior("a", { a: null }),
};
const localMembership = createLocalMembership$({
scope,
...defaultCreateLocalMemberValues,
connectionManager: mockConnectionManager,
localTransport$,
localTransport$: behavior("a", { a: aLocalTransport }),
});
localMembership.requestJoinAndPublish();
@@ -245,10 +257,62 @@ describe("LocalMembership", () => {
});
});
it("Should not publish to active transport if advertised has errors", () => {
withTestScheduler(({ scope, hot, behavior, expectObservable }) => {
const advertised$ = scope.behavior<null | LivekitTransportConfig>(
hot("--#", {}, new FailToGetOpenIdToken(new Error("foo"))),
null,
);
// Populate a connection for active
const connectionManagerData = new ConnectionManagerData();
connectionManagerData.add(connectionTransportBConnected, []);
const mockConnectionManager = {
transports$: constant(new Epoch([bTransport])),
connectionManagerData$: constant(new Epoch(connectionManagerData)),
};
const aLocalTransport: LocalTransport = {
advertised$,
active$: behavior("a", { n: null, a: bTransportWithSFUConfig }),
};
defaultCreateLocalMemberValues.createPublisherFactory.mockImplementation(
() => {
return {} as unknown as Publisher;
},
);
const publisherFactory =
defaultCreateLocalMemberValues.createPublisherFactory as ReturnType<
typeof vi.fn
>;
const localMembership = createLocalMembership$({
scope,
...defaultCreateLocalMemberValues,
connectionManager: mockConnectionManager,
localTransport$: behavior("a", { a: aLocalTransport }),
});
localMembership.requestJoinAndPublish();
expectObservable(localMembership.localMemberState$).toBe("n-e", {
n: TransportState.Waiting,
e: expect.toSatisfy((e) => e instanceof FailToGetOpenIdToken),
});
// Should not have created any publisher
expect(publisherFactory).toHaveBeenCalledTimes(0);
});
});
it("logs if callIntent cannot be updated", async () => {
const scope = new ObservableScope();
const localTransport$ = new BehaviorSubject(aTransport);
const aLocalTransport: LocalTransport = {
advertised$: new BehaviorSubject(aTransport),
active$: new BehaviorSubject(aTransportWithSFUConfig),
};
const mockConnectionManager = {
transports$: constant(new Epoch([])),
connectionManagerData$: constant(new Epoch(new ConnectionManagerData())),
@@ -264,7 +328,7 @@ describe("LocalMembership", () => {
leaveRoomSession: vi.fn(),
},
connectionManager: mockConnectionManager,
localTransport$,
localTransport$: new BehaviorSubject(aLocalTransport),
});
const expextedLog =
"'not connected yet' while updating the call intent (this is expected on startup)";
@@ -279,10 +343,31 @@ describe("LocalMembership", () => {
const aTransport = {
livekit_service_url: "a",
} as LivekitTransportConfig;
const aTransportWithSFUConfig = {
transport: aTransport,
sfuConfig: {
jwt: "foo",
livekitAlias: "bar",
livekitIdentity: "baz",
url: "bro",
},
} as LocalTransportWithSFUConfig;
const bTransport = {
livekit_service_url: "b",
} as LivekitTransportConfig;
const bTransportWithSFUConfig = {
transport: bTransport,
sfuConfig: {
jwt: "foo2",
livekitAlias: "bar2",
livekitIdentity: "baz2",
url: "bro2",
},
} as LocalTransportWithSFUConfig;
const connectionTransportAConnected = {
livekitRoom: mockLivekitRoom({
localParticipant: {
@@ -307,7 +392,11 @@ describe("LocalMembership", () => {
it("recreates publisher if new connection is used, always unpublish and end tracks", async () => {
const scope = new ObservableScope();
const localTransport$ = new BehaviorSubject(aTransport);
const activeTransport$ = new BehaviorSubject(aTransportWithSFUConfig);
const aLocalTransport: LocalTransport = {
advertised$: new BehaviorSubject(aTransport),
active$: activeTransport$,
};
const publishers: Publisher[] = [];
let seed = 0;
@@ -343,10 +432,13 @@ describe("LocalMembership", () => {
connectionManager: {
connectionManagerData$: constant(new Epoch(connectionManagerData)),
},
localTransport$,
localTransport$: new BehaviorSubject(aLocalTransport),
});
await flushPromises();
localTransport$.next(bTransport);
activeTransport$.next({
...aTransportWithSFUConfig,
transport: bTransport,
});
await flushPromises();
expect(publisherFactory).toHaveBeenCalledTimes(2);
@@ -368,8 +460,6 @@ describe("LocalMembership", () => {
it("only start tracks if requested", async () => {
const scope = new ObservableScope();
const localTransport$ = new BehaviorSubject(aTransport);
const publishers: Publisher[] = [];
const tracks$ = new BehaviorSubject<LocalTrack[]>([]);
@@ -396,6 +486,11 @@ describe("LocalMembership", () => {
typeof vi.fn
>;
const aLocalTransport: LocalTransport = {
advertised$: new BehaviorSubject(aTransport),
active$: new BehaviorSubject(aTransportWithSFUConfig),
};
const connectionManagerData = new ConnectionManagerData();
connectionManagerData.add(connectionTransportAConnected, []);
// connectionManagerData.add(connectionTransportB, []);
@@ -405,7 +500,7 @@ describe("LocalMembership", () => {
connectionManager: {
connectionManagerData$: constant(new Epoch(connectionManagerData)),
},
localTransport$,
localTransport$: new BehaviorSubject(aLocalTransport),
});
await flushPromises();
expect(publisherFactory).toHaveBeenCalledOnce();
@@ -428,9 +523,15 @@ describe("LocalMembership", () => {
const scope = new ObservableScope();
const connectionManagerData = new ConnectionManagerData();
const localTransport$ = new BehaviorSubject<null | LivekitTransportConfig>(
null,
);
const activeTransport$ =
new BehaviorSubject<null | LocalTransportWithSFUConfig>(null);
const aLocalTransport: LocalTransport = {
advertised$: new BehaviorSubject(aTransport),
active$: activeTransport$,
};
const connectionManagerData$ = new BehaviorSubject(
new Epoch(connectionManagerData),
);
@@ -470,14 +571,14 @@ describe("LocalMembership", () => {
connectionManager: {
connectionManagerData$,
},
localTransport$,
localTransport$: new BehaviorSubject(aLocalTransport),
});
await flushPromises();
expect(localMembership.localMemberState$.value).toStrictEqual(
TransportState.Waiting,
);
localTransport$.next(aTransport);
activeTransport$.next(aTransportWithSFUConfig);
await flushPromises();
expect(localMembership.localMemberState$.value).toStrictEqual({
matrix: RTCMemberStatus.Connected,
@@ -62,6 +62,8 @@ import {
} from "../remoteMembers/Connection.ts";
import { type HomeserverConnected } from "./HomeserverConnected.ts";
import { and$ } from "../../../utils/observable.ts";
import { type LocalTransport } from "./LocalTransport.ts";
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
export enum TransportState {
/** Not even a transport is available to the LocalMembership */
@@ -127,7 +129,7 @@ interface Props {
createPublisherFactory: (connection: Connection) => Publisher;
joinMatrixRTC: (transport: LivekitTransportConfig) => void;
homeserverConnected: HomeserverConnected;
localTransport$: Behavior<LivekitTransportConfig | null>;
localTransport$: Behavior<LocalTransport>;
matrixRTCSession: Pick<
MatrixRTCSession,
"updateCallIntent" | "leaveRoomSession"
@@ -160,7 +162,7 @@ interface Props {
export const createLocalMembership$ = ({
scope,
connectionManager,
localTransport$: localTransportCanThrow$,
localTransport$,
homeserverConnected,
createPublisherFactory,
joinMatrixRTC,
@@ -205,23 +207,43 @@ export const createLocalMembership$ = ({
const logger = parentLogger.getChild("[LocalMembership]");
logger.debug(`Creating local membership..`);
// Unwrap the local transport and set the state of the LocalMembership to error in case the transport is an error.
const localTransport$ = scope.behavior(
localTransportCanThrow$.pipe(
catchError((e: unknown) => {
// We consider error on the transport as fatal.
// Whether it is the active transport or the preferred transport.
const handleTransportError = (e: unknown): Observable<null> => {
let error: ElementCallError;
if (e instanceof ElementCallError) {
error = e;
} else {
error = new UnknownCallError(
e instanceof Error
? e
: new Error("Unknown error from localTransport"),
e instanceof Error ? e : new Error("Unknown error from localTransport"),
);
}
setTransportError(error);
return of(null);
};
// This is the transport that we will advertise in our membership.
const advertisedTransport$ = localTransport$.pipe(
switchMap((lt) => lt.advertised$),
catchError(handleTransportError),
distinctUntilChanged(areLivekitTransportsEqual),
);
// Unwrap the local transport and set the state of the LocalMembership to error in case the transport is an error.
const activeTransport$ = scope.behavior(
localTransport$.pipe(
switchMap((lt) => {
return combineLatest([lt.active$, lt.advertised$]).pipe(
map(([active, advertised]) => {
// Our policy is to not publish to another transport if our prefered transport is miss-configured
if (advertised == null) return null;
return active?.transport ?? null;
}),
);
}),
catchError(handleTransportError),
distinctUntilChanged(areLivekitTransportsEqual),
),
);
@@ -229,7 +251,7 @@ export const createLocalMembership$ = ({
const localConnection$ = scope.behavior(
combineLatest([
connectionManager.connectionManagerData$,
localTransport$,
activeTransport$,
]).pipe(
map(([{ value: connectionData }, localTransport]) => {
if (localTransport === null) {
@@ -398,7 +420,7 @@ export const createLocalMembership$ = ({
const mediaState$: Behavior<LocalMemberMediaState> = scope.behavior(
combineLatest([
localConnectionState$,
localTransport$,
activeTransport$,
joinAndPublishRequested$,
from(trackStartRequested.promise).pipe(
map(() => true),
@@ -537,9 +559,11 @@ export const createLocalMembership$ = ({
});
});
// Keep matrix rtc session in sync with localTransport$, connectRequested$
// Keep matrix rtc session in sync with advertisedTransport$, connectRequested$
scope.reconcile(
scope.behavior(combineLatest([localTransport$, joinAndPublishRequested$])),
scope.behavior(
combineLatest([advertisedTransport$, joinAndPublishRequested$]),
),
async ([transport, shouldConnect]) => {
if (!transport) return;
// if shouldConnect=false we will do the disconnect as the cleanup from the previous reconcile iteration.
@@ -106,7 +106,7 @@ export function isLocalTransportWithSFUConfig(
return "transport" in obj && "sfuConfig" in obj;
}
interface LocalTransport {
export interface LocalTransport {
/**
* The transport to be advertised in our MatrixRTC membership. `null` when not
* yet fetched/validated.
+4 -2
View File
@@ -82,9 +82,9 @@ unconditionally select the container so we can use cqmin units */
);
inset: var(--fg-inset);
display: grid;
grid-template-columns: 30px 1fr 30px;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr auto;
grid-template-areas: "status status reactions" "nameTag nameTag button";
grid-template-areas: "status reactions" "nameTag button";
gap: var(--cpd-space-1x);
place-items: start;
}
@@ -125,6 +125,7 @@ unconditionally select the container so we can use cqmin units */
.reactions {
grid-area: reactions;
place-self: start end;
display: flex;
gap: var(--cpd-space-1x);
}
@@ -192,4 +193,5 @@ unconditionally select the container so we can use cqmin units */
.fg > button:first-of-type {
grid-area: button;
place-self: end;
}
+11 -11
View File
@@ -6455,9 +6455,9 @@ __metadata:
languageName: node
linkType: hard
"@vector-im/compound-design-tokens@npm:^9.0.0":
version: 9.0.0
resolution: "@vector-im/compound-design-tokens@npm:9.0.0"
"@vector-im/compound-design-tokens@npm:^10.0.0":
version: 10.1.0
resolution: "@vector-im/compound-design-tokens@npm:10.1.0"
peerDependencies:
"@types/react": "*"
react: ^17 || ^18 || ^19.0.0
@@ -6466,13 +6466,13 @@ __metadata:
optional: true
react:
optional: true
checksum: 10c0/6c53770bfba512d8a9f330ca2d0c481806e35f40d98f53815716e41ddac74d6fc3c4788fcda2e33907d62d2c5c04e64db62176c04513fbee41c7c436730081ce
checksum: 10c0/ffd8223195eac66bcddd85eb5d6cf64ba5ffa521d3673c1caa5346d0346cfe165819d4744b7c1ad6289f8074684a4c4e8f972a54d34ffa91625f1e377e732ad5
languageName: node
linkType: hard
"@vector-im/compound-web@npm:^8.0.0":
version: 8.4.0
resolution: "@vector-im/compound-web@npm:8.4.0"
"@vector-im/compound-web@npm:^9.0.0":
version: 9.0.1
resolution: "@vector-im/compound-web@npm:9.0.1"
dependencies:
"@floating-ui/react": "npm:^0.27.0"
"@radix-ui/react-context-menu": "npm:^2.2.16"
@@ -6487,12 +6487,12 @@ __metadata:
"@fontsource/inconsolata": ^5
"@fontsource/inter": ^5
"@types/react": "*"
"@vector-im/compound-design-tokens": ">=1.6.1 <7.0.0"
"@vector-im/compound-design-tokens": ">=1.6.1 <11.0.0"
react: ^18 || ^19.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10c0/31b73555c47b373d4250872bfe863a030b487197bf1198e3cf3a1ec344f2b02f0c72c1513bb598c1cbd7a91d3c6a334d0c8ae37bd7c90d4859c864fc223e059a
checksum: 10c0/4ae90e1518001a8a4a2a76e86cfc4fc2bba9b2e556c6e4b56df2c7b04e0e2cdb213b70ee7c7ba4288250c726e8182a473d3d5fdf95890451675239c957251eee
languageName: node
linkType: hard
@@ -8666,8 +8666,8 @@ __metadata:
"@typescript-eslint/eslint-plugin": "npm:^8.31.0"
"@typescript-eslint/parser": "npm:^8.31.0"
"@use-gesture/react": "npm:^10.2.11"
"@vector-im/compound-design-tokens": "npm:^9.0.0"
"@vector-im/compound-web": "npm:^8.0.0"
"@vector-im/compound-design-tokens": "npm:^10.0.0"
"@vector-im/compound-web": "npm:^9.0.0"
"@vitejs/plugin-react": "npm:^4.0.1"
"@vitest/coverage-v8": "npm:^4.0.18"
babel-plugin-transform-vite-meta-env: "npm:^1.0.3"