@placetime/corptime-conference 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@placetime/corptime-conference",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "React Native SDK from Corptime-Conference",
5
5
  "main": "index.tsx",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,225 @@
1
+ import { AnyAction } from 'redux';
2
+
3
+ import { IStore } from '../../app/types';
4
+ import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
5
+ import { setUserFilmstripWidth } from '../../filmstrip/actions.web';
6
+ import { getFeatureFlag } from '../flags/functions';
7
+ import MiddlewareRegistry from '../redux/MiddlewareRegistry';
8
+ import { updateSettings } from '../settings/actions';
9
+
10
+ import { OVERWRITE_CONFIG, SET_CONFIG } from './actionTypes';
11
+ import { updateConfig } from './actions';
12
+ import { IConfig } from './configType';
13
+
14
+ /**
15
+ * The middleware of the feature {@code base/config}.
16
+ *
17
+ * @param {Store} store - The redux store.
18
+ * @private
19
+ * @returns {Function}
20
+ */
21
+ MiddlewareRegistry.register(store => next => action => {
22
+ switch (action.type) {
23
+ case SET_CONFIG:
24
+ return _setConfig(store, next, action);
25
+
26
+ case SET_DYNAMIC_BRANDING_DATA:
27
+ return _setDynamicBrandingData(store, next, action);
28
+
29
+ case OVERWRITE_CONFIG:
30
+ return _updateSettings(store, next, action);
31
+
32
+ }
33
+
34
+ return next(action);
35
+ });
36
+
37
+ /**
38
+ * Notifies the feature {@code base/config} that the {@link SET_CONFIG} redux
39
+ * action is being {@code dispatch}ed in a specific redux store.
40
+ *
41
+ * @param {Store} store - The redux store in which the specified {@code action}
42
+ * is being dispatched.
43
+ * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
44
+ * specified {@code action} in the specified {@code store}.
45
+ * @param {Action} action - The redux action which is being {@code dispatch}ed
46
+ * in the specified {@code store}.
47
+ * @private
48
+ * @returns {*} The return value of {@code next(action)}.
49
+ */
50
+ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
51
+ // The reducer is doing some alterations to the config passed in the action,
52
+ // so make sure it's the final state by waiting for the action to be
53
+ // reduced.
54
+ const result = next(action);
55
+ const state = getState();
56
+
57
+ // Update the config with user defined settings.
58
+ const settings = state['features/base/settings'];
59
+ const config: IConfig = {};
60
+
61
+ if (typeof settings.disableP2P !== 'undefined') {
62
+ config.p2p = { enabled: !settings.disableP2P };
63
+ }
64
+
65
+ const resolutionFlag = getFeatureFlag(state, 'resolution');
66
+
67
+ if (typeof resolutionFlag !== 'undefined') {
68
+ config.resolution = resolutionFlag;
69
+ }
70
+
71
+ if (action.config.doNotFlipLocalVideo === true) {
72
+ dispatch(updateSettings({
73
+ localFlipX: false
74
+ }));
75
+ }
76
+
77
+ if (action.config.disableSelfView !== undefined) {
78
+ dispatch(updateSettings({
79
+ disableSelfView: action.config.disableSelfView
80
+ }));
81
+ }
82
+
83
+ const { initialWidth, stageFilmstripParticipants } = action.config.filmstrip || {};
84
+
85
+ if (stageFilmstripParticipants !== undefined) {
86
+ dispatch(updateSettings({
87
+ maxStageParticipants: stageFilmstripParticipants
88
+ }));
89
+ }
90
+
91
+ if (initialWidth) {
92
+ dispatch(setUserFilmstripWidth(initialWidth));
93
+ }
94
+
95
+ dispatch(updateConfig(config));
96
+
97
+ // FIXME On Web we rely on the global 'config' variable which gets altered
98
+ // multiple times, before it makes it to the reducer. At some point it may
99
+ // not be the global variable which is being modified anymore due to
100
+ // different merge methods being used along the way. The global variable
101
+ // must be synchronized with the final state resolved by the reducer.
102
+ if (typeof window.config !== 'undefined') {
103
+ window.config = state['features/base/config'];
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * Updates config based on dynamic branding data.
111
+ *
112
+ * @param {Store} store - The redux store in which the specified {@code action}
113
+ * is being dispatched.
114
+ * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
115
+ * specified {@code action} in the specified {@code store}.
116
+ * @param {Action} action - The redux action which is being {@code dispatch}ed
117
+ * in the specified {@code store}.
118
+ * @private
119
+ * @returns {*} The return value of {@code next(action)}.
120
+ */
121
+ function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: AnyAction) {
122
+ const config: IConfig = {};
123
+ const {
124
+ customParticipantMenuButtons,
125
+ customToolbarButtons,
126
+ downloadAppsUrl,
127
+ etherpadBase,
128
+ liveStreamingDialogUrls = {},
129
+ preCallTest = {},
130
+ salesforceUrl,
131
+ userDocumentationUrl,
132
+ peopleSearchUrl,
133
+ } = action.value;
134
+
135
+ const { helpUrl, termsUrl, dataPrivacyUrl } = liveStreamingDialogUrls;
136
+
137
+ if (helpUrl || termsUrl || dataPrivacyUrl) {
138
+ config.liveStreaming = {};
139
+ if (helpUrl) {
140
+ config.liveStreaming.helpLink = helpUrl;
141
+ }
142
+
143
+ if (termsUrl) {
144
+ config.liveStreaming.termsLink = termsUrl;
145
+ }
146
+
147
+ if (dataPrivacyUrl) {
148
+ config.liveStreaming.dataPrivacyLink = dataPrivacyUrl;
149
+ }
150
+ }
151
+
152
+ if (downloadAppsUrl || userDocumentationUrl) {
153
+ config.deploymentUrls = {};
154
+
155
+ if (downloadAppsUrl) {
156
+ config.deploymentUrls.downloadAppsUrl = downloadAppsUrl;
157
+ }
158
+
159
+ if (userDocumentationUrl) {
160
+ config.deploymentUrls.userDocumentationURL = userDocumentationUrl;
161
+ }
162
+ }
163
+
164
+ if (salesforceUrl) {
165
+ config.salesforceUrl = salesforceUrl;
166
+ }
167
+
168
+ if (peopleSearchUrl) {
169
+ config.peopleSearchUrl = peopleSearchUrl;
170
+ }
171
+
172
+ const { enabled, iceUrl } = preCallTest;
173
+
174
+ if (typeof enabled === 'boolean') {
175
+ config.prejoinConfig = {
176
+ preCallTestEnabled: enabled
177
+ };
178
+ }
179
+
180
+ if (etherpadBase) {
181
+ // eslint-disable-next-line camelcase
182
+ config.etherpad_base = etherpadBase;
183
+ }
184
+
185
+ if (iceUrl) {
186
+ config.prejoinConfig = config.prejoinConfig || {};
187
+ config.prejoinConfig.preCallTestICEUrl = iceUrl;
188
+ }
189
+
190
+ if (customToolbarButtons) {
191
+ config.customToolbarButtons = customToolbarButtons;
192
+ }
193
+
194
+ if (customParticipantMenuButtons) {
195
+ config.customParticipantMenuButtons = customParticipantMenuButtons;
196
+ }
197
+
198
+ dispatch(updateConfig(config));
199
+
200
+ return next(action);
201
+ }
202
+
203
+ /**
204
+ * Updates settings based on some config values.
205
+ *
206
+ * @param {Store} store - The redux store in which the specified {@code action}
207
+ * is being dispatched.
208
+ * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
209
+ * specified {@code action} in the specified {@code store}.
210
+ * @param {Action} action - The redux action which is being {@code dispatch}ed
211
+ * in the specified {@code store}.
212
+ * @private
213
+ * @returns {*} The return value of {@code next(action)}.
214
+ */
215
+ function _updateSettings({ dispatch }: IStore, next: Function, action: AnyAction) {
216
+ const { config: { doNotFlipLocalVideo } } = action;
217
+
218
+ if (doNotFlipLocalVideo === true) {
219
+ dispatch(updateSettings({
220
+ localFlipX: false
221
+ }));
222
+ }
223
+
224
+ return next(action);
225
+ }
@@ -0,0 +1,40 @@
1
+ import { isEqual, sortBy } from 'lodash-es';
2
+
3
+ import { MEDIA_TYPE } from '../media/constants';
4
+ import { getScreenshareParticipantIds } from '../participants/functions';
5
+ import StateListenerRegistry from '../redux/StateListenerRegistry';
6
+
7
+ import { isLocalTrackMuted } from './functions';
8
+
9
+ /**
10
+ * Notifies when the list of currently sharing participants changes.
11
+ */
12
+ StateListenerRegistry.register(
13
+ /* selector */ state => getScreenshareParticipantIds(state),
14
+ /* listener */ (participantIDs, store, previousParticipantIDs) => {
15
+ if (typeof APP !== 'object') {
16
+ return;
17
+ }
18
+
19
+ if (!isEqual(sortBy(participantIDs), sortBy(previousParticipantIDs))) {
20
+ APP.API.notifySharingParticipantsChanged(participantIDs);
21
+ }
22
+ }
23
+ );
24
+
25
+
26
+ /**
27
+ * Notifies when the local video mute state changes.
28
+ */
29
+ StateListenerRegistry.register(
30
+ /* selector */ state => isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO),
31
+ /* listener */ (muted, store, previousMuted) => {
32
+ if (typeof APP !== 'object') {
33
+ return;
34
+ }
35
+
36
+ if (muted !== previousMuted) {
37
+ APP.API.notifyVideoMutedStatusChanged(muted);
38
+ }
39
+ }
40
+ );
@@ -8,7 +8,7 @@ const recipientContainer = {
8
8
  backgroundColor: BaseTheme.palette.support05,
9
9
  borderRadius: BaseTheme.shape.borderRadius,
10
10
  flexDirection: 'row',
11
- height: 48,
11
+ minHeight: 48,
12
12
  marginBottom: BaseTheme.spacing[3],
13
13
  marginHorizontal: BaseTheme.spacing[3],
14
14
  padding: BaseTheme.spacing[2]
@@ -0,0 +1,26 @@
1
+ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
2
+
3
+ import { SELECT_LARGE_VIDEO_PARTICIPANT } from './actionTypes';
4
+ import { selectParticipantInLargeVideo } from './actions.any';
5
+ import { shouldHideLargeVideo } from './functions';
6
+
7
+ /**
8
+ * Updates the large video when transitioning from a hidden state to visible state.
9
+ * This ensures the large video is properly updated when exiting tile view, stage filmstrip,
10
+ * whiteboard, or etherpad editing modes.
11
+ */
12
+ StateListenerRegistry.register(
13
+ /* selector */ state => shouldHideLargeVideo(state),
14
+ /* listener */ (isHidden, { dispatch }) => {
15
+ // When transitioning from hidden to visible state, select participant (because currently it is undefined).
16
+ // Otherwise set it to undefined because we don't show the large video.
17
+ if (!isHidden) {
18
+ dispatch(selectParticipantInLargeVideo());
19
+ } else {
20
+ dispatch({
21
+ type: SELECT_LARGE_VIDEO_PARTICIPANT,
22
+ participantId: undefined
23
+ });
24
+ }
25
+ }
26
+ );
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { BackHandler, Text, TextStyle, View, ViewStyle } from 'react-native';
3
3
  import { connect } from 'react-redux';
4
+ import { ScrollView } from 'react-native-gesture-handler';
4
5
  import { appNavigate } from '../../../app/actions.native';
5
6
 
6
7
  import { IReduxState } from '../../../app/types';
@@ -91,7 +92,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
91
92
  const isTablet = Math.min(_clientWidth, _clientHeight) >= 768;
92
93
 
93
94
  let contentContainerStyles = preJoinStyles.lobbyContentContainer;
94
- let largeVideoContainerStyles = preJoinStyles.largeVideoContainer;
95
+ let largeVideoContainerStyles = preJoinStyles.lobbyLargeVideoContainer;
95
96
 
96
97
  if (isTablet && _aspectRatio === ASPECT_RATIO_WIDE) {
97
98
  // @ts-ignore
@@ -105,23 +106,25 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
105
106
  addBottomPadding = { false }
106
107
  safeAreaInsets = { [ 'right' ] }
107
108
  style = { preJoinStyles.contentWrapper }>
108
- <BrandingImageBackground />
109
- <View style = { largeVideoContainerStyles as ViewStyle }>
110
- <View style = { preJoinStyles.conferenceInfo as ViewStyle }>
111
- <View style = { preJoinStyles.displayRoomNameBackdrop }>
112
- <Text
113
- numberOfLines = { 1 }
114
- style = { preJoinStyles.preJoinRoomName }>
115
- { _roomName }
116
- </Text>
109
+ <ScrollView style={{ flex: 1 }}>
110
+ <BrandingImageBackground />
111
+ <View style = { largeVideoContainerStyles as ViewStyle }>
112
+ <View style = { preJoinStyles.conferenceInfo as ViewStyle }>
113
+ <View style = { preJoinStyles.displayRoomNameBackdrop }>
114
+ <Text
115
+ numberOfLines = { 1 }
116
+ style = { preJoinStyles.preJoinRoomName }>
117
+ { _roomName }
118
+ </Text>
119
+ </View>
117
120
  </View>
121
+ <LargeVideo />
118
122
  </View>
119
- <LargeVideo />
120
- </View>
121
- <View style = { contentContainerStyles as ViewStyle }>
122
- { this._renderToolbarButtons() }
123
- { this._renderContent() }
124
- </View>
123
+ <View style = { contentContainerStyles as ViewStyle }>
124
+ { this._renderToolbarButtons() }
125
+ { this._renderContent() }
126
+ </View>
127
+ </ScrollView>
125
128
  </JitsiScreen>
126
129
  );
127
130
  }
@@ -0,0 +1,282 @@
1
+ // Regex constants for efficient reuse across selector parsing
2
+ const SIMPLE_TAG_NAME_REGEX = /^[a-zA-Z][\w-]*$/;
3
+ const MULTI_ATTRIBUTE_SELECTOR_REGEX = /^([a-zA-Z][\w-]*)?(\[(?:\*\|)?([^=\]]+)=["']?([^"'\]]+)["']?\])+$/;
4
+ const SINGLE_ATTRIBUTE_REGEX = /\[(?:\*\|)?([^=\]]+)=["']?([^"'\]]+)["']?\]/g;
5
+ const WHITESPACE_AROUND_COMBINATOR_REGEX = /\s*>\s*/g;
6
+
7
+ /**
8
+ * Parses a CSS selector into reusable components.
9
+ *
10
+ * @param {string} selector - The CSS selector to parse.
11
+ * @returns {Object} - Object with tagName and attrConditions properties.
12
+ */
13
+ function _parseSelector(selector) {
14
+ // Wildcard selector
15
+ if (selector === '*') {
16
+ return {
17
+ tagName: null, // null means match all tag names
18
+ attrConditions: []
19
+ };
20
+ }
21
+
22
+ // Simple tag name
23
+ if (SIMPLE_TAG_NAME_REGEX.test(selector)) {
24
+ return {
25
+ tagName: selector,
26
+ attrConditions: []
27
+ };
28
+ }
29
+
30
+ // Attribute selector: tagname[attr="value"] or
31
+ // tagname[attr1="value1"][attr2="value2"] (with optional wildcard namespace)
32
+ const multiAttrMatch = selector.match(MULTI_ATTRIBUTE_SELECTOR_REGEX);
33
+
34
+ if (multiAttrMatch) {
35
+ const tagName = multiAttrMatch[1];
36
+ const attrConditions = [];
37
+ let attrMatch;
38
+
39
+ while ((attrMatch = SINGLE_ATTRIBUTE_REGEX.exec(selector)) !== null) {
40
+ attrConditions.push({
41
+ name: attrMatch[1], // This properly strips the *| prefix
42
+ value: attrMatch[2]
43
+ });
44
+ }
45
+
46
+ return {
47
+ tagName,
48
+ attrConditions
49
+ };
50
+ }
51
+
52
+ // Unsupported selector
53
+ throw new SyntaxError(`Unsupported selector pattern: '${selector}'`);
54
+ }
55
+
56
+ /**
57
+ * Filters elements by selector pattern and handles findFirst logic.
58
+ *
59
+ * @param {Element[]} elements - Array of elements to filter.
60
+ * @param {string} selector - CSS selector to match against.
61
+ * @param {boolean} findFirst - If true, return after finding the first match.
62
+ * @returns {Element[]|Element|null} - Filtered results with proper return type.
63
+ */
64
+ function _filterAndMatchElements(elements, selector, findFirst) {
65
+ const { tagName, attrConditions } = _parseSelector(selector);
66
+
67
+ const results = [];
68
+
69
+ for (const element of elements) {
70
+ // Check tag name if specified
71
+ if (tagName && !(element.localName === tagName || element.tagName === tagName)) {
72
+ continue;
73
+ }
74
+
75
+ // Check if all attribute conditions match
76
+ const allMatch = attrConditions.every(condition =>
77
+ element.getAttribute(condition.name) === condition.value
78
+ );
79
+
80
+ if (allMatch) {
81
+ results.push(element);
82
+ if (findFirst) {
83
+ return element;
84
+ }
85
+ }
86
+ }
87
+
88
+ return findFirst ? null : results;
89
+ }
90
+
91
+ /**
92
+ * Handles direct child traversal for selectors with > combinators.
93
+ * This is the shared logic used by both scope selectors and regular direct child selectors.
94
+ *
95
+ * @param {Element[]} startElements - Array of starting elements to traverse from.
96
+ * @param {string[]} selectorParts - Array of selector parts split by '>'.
97
+ * @param {boolean} findFirst - If true, return after finding the first match.
98
+ * @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
99
+ * single Element or null for querySelector.
100
+ */
101
+ function _traverseDirectChildren(startElements, selectorParts, findFirst) {
102
+ let currentElements = startElements;
103
+
104
+ for (const part of selectorParts) {
105
+ const nextElements = [];
106
+
107
+ currentElements.forEach(el => {
108
+ // Get direct children
109
+ const directChildren = Array.from(el.children || []);
110
+
111
+ // Use same helper as handlers
112
+ const matchingChildren = _filterAndMatchElements(directChildren, part, false);
113
+
114
+ nextElements.push(...matchingChildren);
115
+ });
116
+
117
+ currentElements = nextElements;
118
+
119
+ // If we have no results, we can stop early (applies to both querySelector and querySelectorAll)
120
+ if (currentElements.length === 0) {
121
+ return findFirst ? null : [];
122
+ }
123
+ }
124
+
125
+ return findFirst ? currentElements[0] || null : currentElements;
126
+ }
127
+
128
+ /**
129
+ * Handles :scope pseudo-selector cases with direct child combinators.
130
+ *
131
+ * @param {Node} node - The Node which is the root of the tree to query.
132
+ * @param {string} selector - The CSS selector.
133
+ * @param {boolean} findFirst - If true, return after finding the first match.
134
+ * @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
135
+ * single Element or null for querySelector.
136
+ */
137
+ function _handleScopeSelector(node, selector, findFirst) {
138
+ let searchSelector = selector.substring(6);
139
+
140
+ // Handle :scope > tagname (direct children)
141
+ if (searchSelector.startsWith('>')) {
142
+ searchSelector = searchSelector.substring(1);
143
+
144
+ // Split by > and use shared traversal logic
145
+ const parts = searchSelector.split('>');
146
+
147
+ // Start from the node itself (scope)
148
+ return _traverseDirectChildren([ node ], parts, findFirst);
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ /**
155
+ * Handles nested > selectors (direct child combinators).
156
+ *
157
+ * @param {Node} node - The Node which is the root of the tree to query.
158
+ * @param {string} selector - The CSS selector.
159
+ * @param {boolean} findFirst - If true, return after finding the first match.
160
+ * @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
161
+ * single Element or null for querySelector.
162
+ */
163
+ function _handleDirectChildSelectors(node, selector, findFirst) {
164
+ const parts = selector.split('>');
165
+
166
+ // First find elements matching the first part (this could be descendants, not just direct children)
167
+ const startElements = _querySelectorInternal(node, parts[0], false);
168
+
169
+ // If no starting elements found, return early
170
+ if (startElements.length === 0) {
171
+ return findFirst ? null : [];
172
+ }
173
+
174
+ // Use shared traversal logic for the remaining parts
175
+ return _traverseDirectChildren(startElements, parts.slice(1), findFirst);
176
+ }
177
+
178
+ /**
179
+ * Handles simple tag name selectors.
180
+ *
181
+ * @param {Node} node - The Node which is the root of the tree to query.
182
+ * @param {string} selector - The CSS selector.
183
+ * @param {boolean} findFirst - If true, return after finding the first match.
184
+ * @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
185
+ * single Element or null for querySelector.
186
+ */
187
+ function _handleSimpleTagSelector(node, selector, findFirst) {
188
+ const elements = Array.from(node.getElementsByTagName(selector));
189
+
190
+ if (findFirst) {
191
+ return elements[0] || null;
192
+ }
193
+
194
+ return elements;
195
+ }
196
+
197
+ /**
198
+ * Handles attribute selectors: tagname[attr="value"] or tagname[attr1="value1"][attr2="value2"].
199
+ * Supports single or multiple attributes with optional wildcard namespace (*|).
200
+ *
201
+ * @param {Node} node - The Node which is the root of the tree to query.
202
+ * @param {string} selector - The CSS selector.
203
+ * @param {boolean} findFirst - If true, return after finding the first match.
204
+ * @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
205
+ * single Element or null for querySelector.
206
+ */
207
+ function _handleAttributeSelector(node, selector, findFirst) {
208
+ const { tagName } = _parseSelector(selector); // Just to get tagName for optimization
209
+
210
+ // Handler's job: find the right elements to search
211
+ const elementsToCheck = tagName
212
+ ? Array.from(node.getElementsByTagName(tagName))
213
+ : Array.from(node.getElementsByTagName('*'));
214
+
215
+ // Common helper does the matching
216
+ return _filterAndMatchElements(elementsToCheck, selector, findFirst);
217
+ }
218
+
219
+ /**
220
+ * Internal function that implements the core selector matching logic for both
221
+ * querySelector and querySelectorAll. Supports :scope pseudo-selector, direct
222
+ * child selectors, and common CSS selectors.
223
+ *
224
+ * @param {Node} node - The Node which is the root of the tree to query.
225
+ * @param {string} selector - The CSS selector to match elements against.
226
+ * @param {boolean} findFirst - If true, return after finding the first match.
227
+ * @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
228
+ * single Element or null for querySelector.
229
+ */
230
+ function _querySelectorInternal(node, selector, findFirst = false) {
231
+ // Normalize whitespace around > combinators first
232
+ const normalizedSelector = selector.replace(WHITESPACE_AROUND_COMBINATOR_REGEX, '>');
233
+
234
+ // Handle :scope pseudo-selector
235
+ if (normalizedSelector.startsWith(':scope')) {
236
+ return _handleScopeSelector(node, normalizedSelector, findFirst);
237
+ }
238
+
239
+ // Handle nested > selectors (direct child combinators)
240
+ if (normalizedSelector.includes('>')) {
241
+ return _handleDirectChildSelectors(node, normalizedSelector, findFirst);
242
+ }
243
+
244
+ // Fast path: simple tag name
245
+ if (normalizedSelector === '*' || SIMPLE_TAG_NAME_REGEX.test(normalizedSelector)) {
246
+ return _handleSimpleTagSelector(node, normalizedSelector, findFirst);
247
+ }
248
+
249
+ // Attribute selector: tagname[attr="value"] or
250
+ // tagname[attr1="value1"][attr2="value2"] (with optional wildcard namespace)
251
+ if (normalizedSelector.match(MULTI_ATTRIBUTE_SELECTOR_REGEX)) {
252
+ return _handleAttributeSelector(node, normalizedSelector, findFirst);
253
+ }
254
+
255
+ // Unsupported selector - throw SyntaxError to match browser behavior
256
+ throw new SyntaxError(`Failed to execute 'querySelector${
257
+ findFirst ? '' : 'All'}' on 'Element': '${selector}' is not a valid selector.`);
258
+ }
259
+
260
+ /**
261
+ * Implements querySelector functionality using the shared internal logic.
262
+ * Supports the same selectors as querySelectorAll but returns only the first match.
263
+ *
264
+ * @param {Node} node - The Node which is the root of the tree to query.
265
+ * @param {string} selectors - The CSS selector to match elements against.
266
+ * @returns {Element|null} - The first Element which matches the selector, or null.
267
+ */
268
+ export function querySelector(node, selectors) {
269
+ return _querySelectorInternal(node, selectors, true);
270
+ }
271
+
272
+ /**
273
+ * Implements querySelectorAll functionality using the shared internal logic.
274
+ * Supports :scope pseudo-selector, direct child selectors, and common CSS selectors.
275
+ *
276
+ * @param {Node} node - The Node which is the root of the tree to query.
277
+ * @param {string} selector - The CSS selector to match elements against.
278
+ * @returns {Element[]} - Array of Elements matching the selector.
279
+ */
280
+ export function querySelectorAll(node, selector) {
281
+ return _querySelectorInternal(node, selector, false);
282
+ }
@@ -31,6 +31,10 @@ export const preJoinStyles = {
31
31
  height: '50%'
32
32
  },
33
33
 
34
+ lobbyLargeVideoContainer: {
35
+ height: 300
36
+ },
37
+
34
38
  largeVideoContainerWide: {
35
39
  height: '100%',
36
40
  marginRight: 'auto',