openvibers/openvibe.design Commits
Pattern: watch and camera screens own the screen (#40)
Changes
@@@ -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}}@@@ -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'));@@@ -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')],
@@ -34,3 +34,12 @@
@@ -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{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)}
.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}}
@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}}
@@ -51,6 +51,18 @@
@@ -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.');
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') {
} 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);
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') {
} else if (id === 'decor-stays-out') {
const frame = node('div', 'decor-demo'), bar = node('div', 'decor-bar'), head = node('strong', 'decor-head', 'Conductor');
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'));
bar.append(node('span', 'decor-mark', 'ov'), node('span', '', 'Codes'));
@@ -35,6 +35,7 @@ const patterns = [
@@ -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')],
['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')],
['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')],
['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')],
['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')],
['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')],
['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')],