openvibers/openvibe.design Commits

Pattern: watch and camera screens own the screen (#40)

OP OpenVibers committed 13 days ago · 48db2b60c7 · 3 files, +22 −0

On a phone, a screen for watching a stream or going live hides the bottom tab bar and every
floating button; its own controls take the edges (the chat docked under a wide picture, or over
the lower part of a portrait one), and leaving the screen brings the tabs back.

The dated bug (2026-10-31): the phone tabs covered a stream's chat button and the studio's
controls on openvibe.live, and the tabs' own Chat opened the inbox instead of the stream's chat
(fixed in OpenVibe.Live #468). The catalog generates the SwiftUI and Compose examples, so the
native apps follow the same rule. The live demo switches a phone frame between browsing and
watching.

Changes

ddiff --git a/public/css/patterns.css b/public/css/patterns.css
iindex 8503805..dcd5012 100644
---- a/public/css/patterns.css
++++ b/public/css/patterns.css
@@@ -34,3 +34,12 @@
.clear-pill{position:absolute;right:10px;bottom:10px;display:inline-flex;align-items:center;height:42px;padding:0 16px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);font-weight:750;font-size:13px}
.clear-pill::before{content:'';width:8px;height:8px;margin-right:8px;border-radius:50%;background:var(--warning)}
@media(prefers-reduced-motion:reduce){.drag-chip,.clear-toasts{transition:none}}
++.watch-demo{position:relative;display:flex;flex-direction:column;height:240px;max-width:300px;margin:0 auto;border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--bg-primary)}
++.watch-player{position:relative;flex:0 0 auto;aspect-ratio:16/9;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 45%,#000),#000)}
++.watch-live{position:absolute;top:8px;left:8px;padding:2px 8px;border-radius:999px;background:#e11d48;color:#fff;font-size:.72rem;font-weight:800}
++.watch-chat{flex:1 1 0;min-height:0;overflow:auto;padding:8px 12px 64px;font-size:.85rem;border-top:1px solid var(--border)}
++.watch-chat p{margin:0 0 4px}
++.watch-demo .bottom-nav{transition:transform var(--ov-dur-base,260ms) ease}
++.watch-demo.is-watching .bottom-nav{transform:translateY(110%)}
++.watch-demo.is-watching .watch-chat{padding-bottom:8px}
++@media (prefers-reduced-motion:reduce){.watch-demo .bottom-nav{transition:none}}
ddiff --git a/public/js/patterns.js b/public/js/patterns.js
iindex 3440393..d3be2d1 100644
---- a/public/js/patterns.js
++++ b/public/js/patterns.js
@@@ -51,6 +51,18 @@
const nav=node('nav','sample-nav');nav.setAttribute('aria-label','Example page navbar');nav.append(node('strong','','OpenVibe'),node('span','','Patterns'),node('span','','One shared navbar'));widget.append(nav);say('This page itself uses the shared Frame navbar once.');
} else if (id === 'bottom-tabs') {
const frame=node('div','bottom-demo');frame.append(node('strong','','Scrollable content'),node('p','','This content reserves space under the fixed phone tabs.'),node('p','','The final row remains visible above the bar.'));const nav=node('nav','bottom-nav');nav.setAttribute('aria-label','Phone tabs');['Home','Runs','Settings'].forEach(x=>nav.append(button(x,()=>say(`${x} tab selected.`))));frame.append(nav);widget.append(frame);
++ } else if (id === 'media-owns-the-screen') {
++ // A phone frame: browsing shows the tabs; the watch screen hides them and docks the stream's chat under the picture.
++ const frame = node('div', 'watch-demo'), player = node('div', 'watch-player'), chat = node('div', 'watch-chat');
++ player.append(node('span', 'watch-live', '● Live'));
++ ['Ana: hi!', 'Ben: what game is this?', 'Cy: the stream looks great'].forEach((x) => chat.append(node('p', '', x)));
++ const nav = node('nav', 'bottom-nav'); nav.setAttribute('aria-label', 'Phone tabs');
++ ['Home', 'Videos', 'Chat'].forEach((x) => nav.append(button(x, () => say(`${x} tab selected.`))));
++ frame.append(player, chat, nav);
++ widget.append(frame);
++ control('Watch screen', () => { frame.classList.add('is-watching'); say('Watching: the tabs step aside and the stream’s chat takes the room.'); }, true);
++ control('Browse', () => { frame.classList.remove('is-watching'); say('Browsing: the tabs are back.'); });
++ frame.classList.add('is-watching');
} else if (id === 'decor-stays-out') {
const frame = node('div', 'decor-demo'), bar = node('div', 'decor-bar'), head = node('strong', 'decor-head', 'Conductor');
bar.append(node('span', 'decor-mark', 'ov'), node('span', '', 'Codes'));
ddiff --git a/server/patterns/catalog.js b/server/patterns/catalog.js
iindex 9cdbda5..258f621 100644
---- a/server/patterns/catalog.js
++++ b/server/patterns/catalog.js
@@@ -35,6 +35,7 @@ const patterns = [
['site-switcher','Navigation','Site switcher', 'Open the shared site switcher from the single site navbar. Keep its focus, placement, and close behavior consistent across every page.', '2026-10-30 · A local site picker fork drifted from the shared switcher and behaved differently across services.', 'Use openvibe-shared/site-switcher.js and the shared navbar trigger.', t('tap','durationBase','easingStandard')],
['single-navbar','Navigation','One navbar per page', 'Render one shared navbar at the top of each page. Put local section navigation inside the page body instead of adding a second header.', '2026-10-30 · A page-specific header sat beneath the Frame navbar and duplicated the page title.', 'The shared Frame owns the page navbar.', t('spaceMd','tap')],
['bottom-tabs','Navigation','Bottom tabs without overlap', 'On phones, reserve space for a bottom tab bar in the scrollable content. Keep the last item and focused controls clear of the fixed bar and safe area.', '2026-10-30 · The fixed phone tabs covered the final rows and hid the focused control.', 'Scrollable content has enough bottom inset for the tabs and safe area.', t('tap','spaceMd','durationBase')],
++ ['media-owns-the-screen','Navigation','Watch and camera screens own the screen', 'On a phone, a screen for watching a stream or going live hides the bottom tab bar and every floating button. Its own controls take the edges: the chat docks under a wide picture, or runs over the lower part of a portrait one. Leaving the screen brings the tabs back.', '2026-10-31 · The phone tabs covered a stream’s chat button and the studio’s controls, and the tabs’ own Chat opened the inbox instead of the stream’s chat.', 'One chat, no floating buttons, and nothing over the picture but its own controls.', t('tap','spaceMd','durationBase')],
['decor-stays-out','Layout that holds','Decorations stay out of content', 'A decoration (a seasonal fall, confetti, a sparkle) moves only inside the bar or element it belongs to, and settles on that edge. It never crosses a heading, a field or text, and reduced motion turns it off.', '2026-10-30 · Once a day the mark’s leaves fell 120–280 px from the brand, across the page heading on phones (Shared 4.50.1).', 'A flourish lands on its own bar’s edge and fades there.', t('durationSlow','easingStandard','durationReduced')],
['shared-columns','Layout that holds','One set of columns for a list', 'When a phone folds a table into stacked rows, every row uses the same columns. A grid each row sizes for itself makes names and numbers jump from row to row.', '2026-10-30 · Bench’s ratings rows each sized their own grid, so model names started at a different place in every row (Bench #35).', 'Rows of one list line up with each other.', t('spaceMd')],
['numbers-align','Layout that holds','Numbers line up under their header', 'A column of numbers is right-aligned, with tabular figures, and its header aligns the same way. Check that a broader rule does not override the alignment with higher specificity.', '2026-10-30 · Vote counts sat left-aligned under a right-aligned header, because a table-wide td rule outranked the numeric class (Bench #35).', 'Digits stack, and the header sits over them.', t('spaceMd','text')],