diff --git a/frontend/src/components/room/RoomStage.tsx b/frontend/src/components/room/RoomStage.tsx
index f6c5858..4f4939e 100644
--- a/frontend/src/components/room/RoomStage.tsx
+++ b/frontend/src/components/room/RoomStage.tsx
@@ -179,6 +179,7 @@ export function RoomStage({
onHideOthers,
initialFocusKey = null,
onFocusKeyChange,
+ onPinFocus,
}: {
variant?: 'full' | 'pip'
/** Выбранный пользователем режим показа; игнорируется при `variant="pip"`. */
@@ -193,6 +194,14 @@ export function RoomStage({
initialFocusKey?: string | null
/** Сообщать наружу текущий фокус, чтобы его пережил переезд сцены в мини-плеер и обратно. */
onFocusKeyChange?: (key: string | null) => void
+ /**
+ * Участника только что закрепили (не открепили) в режиме без крупной
+ * плитки — сцена сама переключиться не может (режим живёт в `RoomPage`),
+ * поэтому просит переключить на `standard`, где закреплённого есть куда
+ * показать крупно. Открепление НЕ вызывает колбэк — вид назад не
+ * переключаем (см. докстринг `RoomPage`, обоснование решения в коммите).
+ */
+ onPinFocus?: () => void
}) {
const room = useRoomContext()
const isCompact = useIsCompactViewport()
@@ -337,9 +346,16 @@ export function RoomStage({
* Закрепить/открепить плитку: повторное нажатие на уже закреплённой снимает
* закрепление. Ключ приходит из самой плитки (она знает свой трек из
* контекста — в карусели/гриде плитки рендерятся шаблоном, без пропсов).
+ *
+ * Закрепление доступно из ЛЮБОГО режима показа (задача 3.1 → 3.3): в
+ * плиточных режимах крупной плитки нет, поэтому само закрепление (не
+ * открепление) просит `RoomPage` переключить вид на `standard` —
+ * закреплённого иначе негде показать крупно.
*/
function handleTogglePin(key: string) {
- setPinnedKey((prev) => (prev === key ? null : key))
+ const willPin = pinnedKey !== key
+ setPinnedKey(willPin ? key : null)
+ if (willPin) onPinFocus?.()
}
// Мини-плеер показывает ТОЛЬКО активное окно — без карусели/
@@ -372,23 +388,23 @@ export function RoomStage({
: []
: carouselTracks
const showCarousel = !hideOthers && sideTracks.length > 0
- // Закрепление имеет смысл только там, где есть «крупная плитка» —
- // в плиточных режимах фокуса нет, поэтому и булавки на плитках нет.
- const pinProps =
- effectiveMode === 'standard' ? { pinnedKey, onTogglePin: handleTogglePin } : {}
+ // Булавка закрепления есть на КАЖДОЙ плитке во всех режимах (задача 3.3):
+ // сама кнопка вызывает переключение на `standard`, где закреплённый и
+ // попадёт в фокус (см. `handleTogglePin`/`onPinFocus`).
+ const pinProps = { pinnedKey, onTogglePin: handleTogglePin }
function renderMain(): ReactNode {
if (effectiveMode === 'tiles') {
return (
-
+
)
}
if (effectiveMode === 'live-tiles') {
return (
0 ? liveCameraTracks : cameraTracks}>
-
+
)
}
diff --git a/frontend/src/pages/RoomPage.tsx b/frontend/src/pages/RoomPage.tsx
index 18b330e..f807a71 100644
--- a/frontend/src/pages/RoomPage.tsx
+++ b/frontend/src/pages/RoomPage.tsx
@@ -169,6 +169,15 @@ export function RoomPage() {
saveStageLayoutMode(mode)
}, [])
+ // Закрепление участника из плиточного режима переключает вид на «Стандарт»
+ // (см. докстринг `RoomStage`, `onPinFocus`) — но это НЕ то же самое, что
+ // явный выбор пользователя: переключение не сохраняется в localStorage
+ // (в отличие от `handleLayoutModeChange`), иначе разовое закрепление в этой
+ // встрече поменяло бы устойчивое предпочтение режима для всех следующих.
+ const handlePinFocus = useCallback(() => {
+ setLayoutMode('standard')
+ }, [])
+
// Ключ трека, который сцена показывает крупно. Живёт ЗДЕСЬ, а не только
// внутри `RoomStage`, потому что при открытии мини-плеера сцена
// размонтируется в основном окне и монтируется заново в PiP-окне (портал
@@ -267,6 +276,7 @@ export function RoomPage() {
onHideOthers={() => setHideOthers(true)}
initialFocusKey={stageFocusKey}
onFocusKeyChange={setStageFocusKey}
+ onPinFocus={handlePinFocus}
/>
)}
{chatVisible && chatOpen && (