openvibers/openvibe.design Commits
One menu button: the patterns page and rule draw the new button (#249)
Changes
@@@ -168,11 +168,20 @@control('Mixed with the text', () => { row.classList.remove('broken'); say('Half the hue, half the text: readable on both.'); }, true);control('The bug', () => { row.classList.add('broken'); say('The pure hue: fine on dark, 1.7:1 for gold on white.'); });} else if (id === 'one-menu') {-- const box = node('div', 'menu-demo'); const bar = node('div', 'menu-bar'); const orb = button('✦', () => open('orb')); orb.setAttribute('aria-label', 'Open from the orb'); const burger = button('☰', () => open('burger')); burger.setAttribute('aria-label', 'Open from the burger');-- bar.append(orb, node('strong', '', 'OpenVibe.Live'), node('span', 'menu-gap'), burger); const panel = node('ol', 'menu-panel'); box.append(bar, panel); widget.append(box);-- const order = { burger: ['This site’s pages', 'Live now', 'Your places', 'Your workspace', 'From the sites', 'What’s new', 'Every site (54)', 'Jump back in'], orb: ['Your places', 'Live now', 'Your workspace', 'From the sites', 'What’s new', 'This site’s pages', 'Every site (54)', 'Jump back in'] };-- function open(from) { panel.replaceChildren(...order[from].map((t) => node('li', '', t))); box.classList.add('open'); say(from === 'burger' ? 'From the burger: this site’s pages first.' : 'From the orb: your places first.'); }-- control('Open from the burger', () => open('burger'), true); control('Back', () => { box.classList.remove('open'); say('Back closed the menu; the page stayed.'); });++ // The owner's menu button (openvibe-shared 4.187.0, root.OpenVibeOrb.svg): a search bar over three apps.++ // Drawn by the shared navbar when it is on the page, else the same shapes inline.++ const orb = (open) => (window.OpenVibeOrb ? window.OpenVibeOrb.svg(20, open)++ : `<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="currentColor"><rect x="4" y="5.2" width="16" height="5.6" rx="2.8" fill="none" stroke="currentColor" stroke-width="1.9"/><circle cx="7.4" cy="8" r="1.15"/><rect x="4.2" y="14.6" width="4" height="4" rx="1.3"/><rect x="10" y="14.6" width="4" height="4" rx="1.3"/><rect x="15.8" y="14.6" width="4" height="4" rx="1.3"/></svg>`);++ const box = node('div', 'menu-demo'); const bar = node('div', 'menu-bar');++ const menu = node('button', 'dbtn menu-orb'); menu.type = 'button'; menu.setAttribute('aria-label', 'The menu button at the top of the page');++ const paint = (open) => { menu.innerHTML = orb(open) + `<span>${open ? 'Close' : 'Menu'}</span>`; menu.setAttribute('aria-expanded', String(open)); };++ paint(false);++ bar.append(node('strong', '', 'OpenVibe.Live'), node('span', 'menu-gap'), menu); const panel = node('ol', 'menu-panel'); box.append(bar, panel); widget.append(box);++ const order = ['This site’s pages', 'Live now', 'Your places', 'Your workspace', 'From the sites', 'What’s new', 'Every site (54)', 'Jump back in'];++ function open(show) { if (show) panel.replaceChildren(...order.map((t) => node('li', '', t))); box.classList.toggle('open', show); paint(show); }++ menu.addEventListener('click', () => open(!box.classList.contains('open')));++ control('Open the menu', () => { open(true); say('The menu button at the top of the page opens the OpenVibe menu on this site: its pages lead.'); }, true);++ control('Back', () => { open(false); say('Back closed the menu; the page stayed.'); });} else if (id === 'site-cards') {const card = node('div', 'card-demo'); const head = node('div', 'card-head'); head.append(node('strong', '', 'Trending'), node('span', 'pmeta', 'Video')); const list = node('ol', 'card-rows'); card.append(head, list); widget.append(card);const add = (title, meta, ok) => { if (!ok) { say('Dropped: the entry’s link is not https on an OpenVibe host.'); return; } const li = node('li', ''); li.append(node('b', '', title), node('small', '', meta)); list.append(li); card.hidden = false; say(`${list.children.length} real entr${list.children.length === 1 ? 'y' : 'ies'}.`); };@@@ -48,7 +48,7 @@ const patterns = [['own-places','Layout that holds','The hero draws its own places', 'A home’s first picture says what this site is: an orbit of its own places, or its own live numbers, every node a link. A generic picture shared by many sites says nothing.', '2026-11-01 · Six homes drew the same rings around the mark, and Search left a tall empty band where its numbers belonged.', 'The first screen shows this site, not the network’s wallpaper.', t('durationSlow','easingStandard','accent')],['mixed-words','Elements','Coloured words mix with the text', 'A word in a hue (a rarity, a status, an AI label) is the hue mixed with the theme’s text, about half and half. It holds on dark and light themes; fills and borders keep the pure hue.', '2026-11-01 · Gold, violet and green words read at 1.4 to 2.5:1 on Daylight.', 'One rule for words in colour, on every theme.', t('text','accent','surface')],['mark-system','Brand','The mark at work', 'Treat the mark as a working component: it shows a page on its way, work, done, failed and live in one shape, and moves only when it has something to say. The mark is the Pair (2026-10-31); its moves, forms and files are on /brand.', '2026-10-30 · A static logo could not communicate loading or adapt to a site identity.', 'The mark carries its identity through loading and site changes.', t('durationSlow','easingStandard','easingBack')],-- ['one-menu','Navigation','One menu, opened from either end','The burger and the orb open the same OpenVibe menu. From the burger, this site’s pages lead; from the orb, your places do. Then who is live, your places, your workspace, what the sites put forward, what is new since your last visit, tools, every site behind a count, and Jump back in last. On a phone it fills the screen, and Back closes it before it leaves the page.', '2026-11-07 · A right-hand drawer and the OpenVibe menu both listed the site’s pages; the site list cut its labels, and Back on a phone left the page with the menu still open.', 'One menu, two doors; the order follows the door.', t('durationBase','easingStandard','tap')],++ ['one-menu','Navigation','One menu button','The menu button at the top of the page opens the OpenVibe menu on the site you are on: this site’s pages lead. Then who is live, your places, your workspace, what the sites put forward, what is new since your last visit, tools, every site behind a count, and Jump back in last. On a phone it fills the screen, and Back closes it before it leaves the page.', '2026-11-07 · A right-hand drawer and the OpenVibe menu both listed the site’s pages; the site list cut its labels, and Back on a phone left the page with the menu still open.', 'One menu, one button; the menu opens on this site.', t('durationBase','easingStandard','tap')],['site-cards','Navigation','Sites put their own content in the menu','A site publishes what it wants in the menu as data (frame.menu-card@1 at /.well-known/openvibe-menu.json): a title, up to eight entries with a link and one short line, and a More link. The Network checks every entry and drops any that does not validate. A card with no real entries is not shown, and never filled with examples.', '2026-11-07 · The menu linked to Video, Games and Live but showed nothing of what was on them, so people opened a site to find out.', 'Real entries from the site itself, checked, or nothing.', t('spaceMd','radiusPill')],['your-things','Navigation','Your things in every bar','For a signed-in person, every site’s bar carries the same three things: Messages with an unread count (they open in place), your balances, and your status on your picture. A site may say no to one, or bring its own, but never a second copy.', '2026-11-07 · DMs and balances showed on Live and Chat only, so on every other site a message meant leaving for another tab.', 'Messages, balances and your status, the same on every site.', t('tap','radiusPill')],['status-dot','Elements','Status is a dot and a word','Show a person’s status as a dot on their picture and a word beside it: online, away, do not disturb, live. To other people, offline and invisible show no dot at all; never show “last seen”. The dot is decoration (aria-hidden), and the picture’s name carries the word.', '2026-11-07 · A profile said “Live now” for someone who was only online, because there was no way to say online.', 'A dot for the eye, a word for everyone, and nothing when away from it.', t('spaceMd','text')],@@@ -145,10 +145,10 @@ const RULES = Object.freeze([[{ label: 'Motion', href: '/motion' }]),// ── Navigation and the Frame ──-- R('one-menu-button', 'navigation', 'One menu button on every site: the orb.',-- 'The nine-dot orb opens the OpenVibe menu; there is never a second three-line button that does the same thing.',++ R('one-menu-button', 'navigation', 'One menu button on every site.',++ 'The menu button at the top of the page opens the OpenVibe menu; there is never a second button that does the same thing.','2026-11-08', 'Two buttons opened the same menu, and the custom orb had been hidden.',-- [{ label: 'One menu, opened from either end', href: '/patterns/one-menu' }]),++ [{ label: 'One menu button', href: '/patterns/one-menu' }]),R('one-bar', 'navigation', 'One navbar per page, owned by the shared Frame.','A site’s own section links go in the page body, never in a second header under the bar.','2026-10-30', 'Page headers under the bar doubled the navigation.',@@@ -25,6 +25,16 @@ const { patterns, TOKENS, nativeSpecs, esc } = require('../server/patterns/catalassert.equal(res.status, 200);for (const name of ['Ribbon', 'Breadcrumb', 'Porthole', 'Liquid', 'Seal']) assert.ok(res.text.includes(name), name);});++ await check('the one-menu pattern draws the menu button at the top of the page, not the old burger', async () => {++ const res = await t.get('/patterns/one-menu');++ assert.equal(res.status, 200);++ assert.ok(res.text.includes('The menu button at the top of the page'), 'the rule names the button');++ assert.ok(!/burger/i.test(res.text), 'no burger in the rule');++ const js = await t.get('/js/patterns.js');++ assert.ok(js.text.includes('window.OpenVibeOrb ? window.OpenVibeOrb.svg(20, open)'), 'drawn by the shared orb');++ assert.ok(js.text.includes('x="4" y="5.2" width="16" height="5.6"'), 'the inline fallback is the same shapes');++ assert.ok(!js.text.includes('☰'), 'no three-line button');++ });await check('the live demo assets are served', async () => {for (const asset of ['/js/pattern-kit.js', '/js/patterns.js', '/css/patterns.css']) {const res = await t.get(asset);
@@ -168,11 +168,20 @@
@@ -168,11 +168,20 @@
control('Mixed with the text', () => { row.classList.remove('broken'); say('Half the hue, half the text: readable on both.'); }, true);
control('Mixed with the text', () => { row.classList.remove('broken'); say('Half the hue, half the text: readable on both.'); }, true);
control('The bug', () => { row.classList.add('broken'); say('The pure hue: fine on dark, 1.7:1 for gold on white.'); });
control('The bug', () => { row.classList.add('broken'); say('The pure hue: fine on dark, 1.7:1 for gold on white.'); });
} else if (id === 'one-menu') {
} else if (id === 'one-menu') {
- const box = node('div', 'menu-demo'); const bar = node('div', 'menu-bar'); const orb = button('✦', () => open('orb')); orb.setAttribute('aria-label', 'Open from the orb'); const burger = button('☰', () => open('burger')); burger.setAttribute('aria-label', 'Open from the burger');
- bar.append(orb, node('strong', '', 'OpenVibe.Live'), node('span', 'menu-gap'), burger); const panel = node('ol', 'menu-panel'); box.append(bar, panel); widget.append(box);
- const order = { burger: ['This site’s pages', 'Live now', 'Your places', 'Your workspace', 'From the sites', 'What’s new', 'Every site (54)', 'Jump back in'], orb: ['Your places', 'Live now', 'Your workspace', 'From the sites', 'What’s new', 'This site’s pages', 'Every site (54)', 'Jump back in'] };
- function open(from) { panel.replaceChildren(...order[from].map((t) => node('li', '', t))); box.classList.add('open'); say(from === 'burger' ? 'From the burger: this site’s pages first.' : 'From the orb: your places first.'); }
- control('Open from the burger', () => open('burger'), true); control('Back', () => { box.classList.remove('open'); say('Back closed the menu; the page stayed.'); });
+ // The owner's menu button (openvibe-shared 4.187.0, root.OpenVibeOrb.svg): a search bar over three apps.
+ // Drawn by the shared navbar when it is on the page, else the same shapes inline.
+ const orb = (open) => (window.OpenVibeOrb ? window.OpenVibeOrb.svg(20, open)
+ : `<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" fill="currentColor"><rect x="4" y="5.2" width="16" height="5.6" rx="2.8" fill="none" stroke="currentColor" stroke-width="1.9"/><circle cx="7.4" cy="8" r="1.15"/><rect x="4.2" y="14.6" width="4" height="4" rx="1.3"/><rect x="10" y="14.6" width="4" height="4" rx="1.3"/><rect x="15.8" y="14.6" width="4" height="4" rx="1.3"/></svg>`);
+ const box = node('div', 'menu-demo'); const bar = node('div', 'menu-bar');
+ const menu = node('button', 'dbtn menu-orb'); menu.type = 'button'; menu.setAttribute('aria-label', 'The menu button at the top of the page');
+ const paint = (open) => { menu.innerHTML = orb(open) + `<span>${open ? 'Close' : 'Menu'}</span>`; menu.setAttribute('aria-expanded', String(open)); };
+ paint(false);
+ bar.append(node('strong', '', 'OpenVibe.Live'), node('span', 'menu-gap'), menu); const panel = node('ol', 'menu-panel'); box.append(bar, panel); widget.append(box);
+ const order = ['This site’s pages', 'Live now', 'Your places', 'Your workspace', 'From the sites', 'What’s new', 'Every site (54)', 'Jump back in'];
+ function open(show) { if (show) panel.replaceChildren(...order.map((t) => node('li', '', t))); box.classList.toggle('open', show); paint(show); }
+ menu.addEventListener('click', () => open(!box.classList.contains('open')));
+ control('Open the menu', () => { open(true); say('The menu button at the top of the page opens the OpenVibe menu on this site: its pages lead.'); }, true);
+ control('Back', () => { open(false); say('Back closed the menu; the page stayed.'); });
} else if (id === 'site-cards') {
} else if (id === 'site-cards') {
const card = node('div', 'card-demo'); const head = node('div', 'card-head'); head.append(node('strong', '', 'Trending'), node('span', 'pmeta', 'Video')); const list = node('ol', 'card-rows'); card.append(head, list); widget.append(card);
const card = node('div', 'card-demo'); const head = node('div', 'card-head'); head.append(node('strong', '', 'Trending'), node('span', 'pmeta', 'Video')); const list = node('ol', 'card-rows'); card.append(head, list); widget.append(card);
const add = (title, meta, ok) => { if (!ok) { say('Dropped: the entry’s link is not https on an OpenVibe host.'); return; } const li = node('li', ''); li.append(node('b', '', title), node('small', '', meta)); list.append(li); card.hidden = false; say(`${list.children.length} real entr${list.children.length === 1 ? 'y' : 'ies'}.`); };
const add = (title, meta, ok) => { if (!ok) { say('Dropped: the entry’s link is not https on an OpenVibe host.'); return; } const li = node('li', ''); li.append(node('b', '', title), node('small', '', meta)); list.append(li); card.hidden = false; say(`${list.children.length} real entr${list.children.length === 1 ? 'y' : 'ies'}.`); };
@@ -48,7 +48,7 @@ const patterns = [
@@ -48,7 +48,7 @@ const patterns = [
['own-places','Layout that holds','The hero draws its own places', 'A home’s first picture says what this site is: an orbit of its own places, or its own live numbers, every node a link. A generic picture shared by many sites says nothing.', '2026-11-01 · Six homes drew the same rings around the mark, and Search left a tall empty band where its numbers belonged.', 'The first screen shows this site, not the network’s wallpaper.', t('durationSlow','easingStandard','accent')],
['own-places','Layout that holds','The hero draws its own places', 'A home’s first picture says what this site is: an orbit of its own places, or its own live numbers, every node a link. A generic picture shared by many sites says nothing.', '2026-11-01 · Six homes drew the same rings around the mark, and Search left a tall empty band where its numbers belonged.', 'The first screen shows this site, not the network’s wallpaper.', t('durationSlow','easingStandard','accent')],
['mixed-words','Elements','Coloured words mix with the text', 'A word in a hue (a rarity, a status, an AI label) is the hue mixed with the theme’s text, about half and half. It holds on dark and light themes; fills and borders keep the pure hue.', '2026-11-01 · Gold, violet and green words read at 1.4 to 2.5:1 on Daylight.', 'One rule for words in colour, on every theme.', t('text','accent','surface')],
['mixed-words','Elements','Coloured words mix with the text', 'A word in a hue (a rarity, a status, an AI label) is the hue mixed with the theme’s text, about half and half. It holds on dark and light themes; fills and borders keep the pure hue.', '2026-11-01 · Gold, violet and green words read at 1.4 to 2.5:1 on Daylight.', 'One rule for words in colour, on every theme.', t('text','accent','surface')],
['mark-system','Brand','The mark at work', 'Treat the mark as a working component: it shows a page on its way, work, done, failed and live in one shape, and moves only when it has something to say. The mark is the Pair (2026-10-31); its moves, forms and files are on /brand.', '2026-10-30 · A static logo could not communicate loading or adapt to a site identity.', 'The mark carries its identity through loading and site changes.', t('durationSlow','easingStandard','easingBack')],
['mark-system','Brand','The mark at work', 'Treat the mark as a working component: it shows a page on its way, work, done, failed and live in one shape, and moves only when it has something to say. The mark is the Pair (2026-10-31); its moves, forms and files are on /brand.', '2026-10-30 · A static logo could not communicate loading or adapt to a site identity.', 'The mark carries its identity through loading and site changes.', t('durationSlow','easingStandard','easingBack')],
- ['one-menu','Navigation','One menu, opened from either end','The burger and the orb open the same OpenVibe menu. From the burger, this site’s pages lead; from the orb, your places do. Then who is live, your places, your workspace, what the sites put forward, what is new since your last visit, tools, every site behind a count, and Jump back in last. On a phone it fills the screen, and Back closes it before it leaves the page.', '2026-11-07 · A right-hand drawer and the OpenVibe menu both listed the site’s pages; the site list cut its labels, and Back on a phone left the page with the menu still open.', 'One menu, two doors; the order follows the door.', t('durationBase','easingStandard','tap')],
+ ['one-menu','Navigation','One menu button','The menu button at the top of the page opens the OpenVibe menu on the site you are on: this site’s pages lead. Then who is live, your places, your workspace, what the sites put forward, what is new since your last visit, tools, every site behind a count, and Jump back in last. On a phone it fills the screen, and Back closes it before it leaves the page.', '2026-11-07 · A right-hand drawer and the OpenVibe menu both listed the site’s pages; the site list cut its labels, and Back on a phone left the page with the menu still open.', 'One menu, one button; the menu opens on this site.', t('durationBase','easingStandard','tap')],
['site-cards','Navigation','Sites put their own content in the menu','A site publishes what it wants in the menu as data (frame.menu-card@1 at /.well-known/openvibe-menu.json): a title, up to eight entries with a link and one short line, and a More link. The Network checks every entry and drops any that does not validate. A card with no real entries is not shown, and never filled with examples.', '2026-11-07 · The menu linked to Video, Games and Live but showed nothing of what was on them, so people opened a site to find out.', 'Real entries from the site itself, checked, or nothing.', t('spaceMd','radiusPill')],
['site-cards','Navigation','Sites put their own content in the menu','A site publishes what it wants in the menu as data (frame.menu-card@1 at /.well-known/openvibe-menu.json): a title, up to eight entries with a link and one short line, and a More link. The Network checks every entry and drops any that does not validate. A card with no real entries is not shown, and never filled with examples.', '2026-11-07 · The menu linked to Video, Games and Live but showed nothing of what was on them, so people opened a site to find out.', 'Real entries from the site itself, checked, or nothing.', t('spaceMd','radiusPill')],
['your-things','Navigation','Your things in every bar','For a signed-in person, every site’s bar carries the same three things: Messages with an unread count (they open in place), your balances, and your status on your picture. A site may say no to one, or bring its own, but never a second copy.', '2026-11-07 · DMs and balances showed on Live and Chat only, so on every other site a message meant leaving for another tab.', 'Messages, balances and your status, the same on every site.', t('tap','radiusPill')],
['your-things','Navigation','Your things in every bar','For a signed-in person, every site’s bar carries the same three things: Messages with an unread count (they open in place), your balances, and your status on your picture. A site may say no to one, or bring its own, but never a second copy.', '2026-11-07 · DMs and balances showed on Live and Chat only, so on every other site a message meant leaving for another tab.', 'Messages, balances and your status, the same on every site.', t('tap','radiusPill')],
['status-dot','Elements','Status is a dot and a word','Show a person’s status as a dot on their picture and a word beside it: online, away, do not disturb, live. To other people, offline and invisible show no dot at all; never show “last seen”. The dot is decoration (aria-hidden), and the picture’s name carries the word.', '2026-11-07 · A profile said “Live now” for someone who was only online, because there was no way to say online.', 'A dot for the eye, a word for everyone, and nothing when away from it.', t('spaceMd','text')],
['status-dot','Elements','Status is a dot and a word','Show a person’s status as a dot on their picture and a word beside it: online, away, do not disturb, live. To other people, offline and invisible show no dot at all; never show “last seen”. The dot is decoration (aria-hidden), and the picture’s name carries the word.', '2026-11-07 · A profile said “Live now” for someone who was only online, because there was no way to say online.', 'A dot for the eye, a word for everyone, and nothing when away from it.', t('spaceMd','text')],
@@ -145,10 +145,10 @@ const RULES = Object.freeze([
@@ -145,10 +145,10 @@ const RULES = Object.freeze([
[{ label: 'Motion', href: '/motion' }]),
[{ label: 'Motion', href: '/motion' }]),
// ── Navigation and the Frame ──
// ── Navigation and the Frame ──
- R('one-menu-button', 'navigation', 'One menu button on every site: the orb.',
- 'The nine-dot orb opens the OpenVibe menu; there is never a second three-line button that does the same thing.',
+ R('one-menu-button', 'navigation', 'One menu button on every site.',
+ 'The menu button at the top of the page opens the OpenVibe menu; there is never a second button that does the same thing.',
'2026-11-08', 'Two buttons opened the same menu, and the custom orb had been hidden.',
'2026-11-08', 'Two buttons opened the same menu, and the custom orb had been hidden.',
- [{ label: 'One menu, opened from either end', href: '/patterns/one-menu' }]),
+ [{ label: 'One menu button', href: '/patterns/one-menu' }]),
R('one-bar', 'navigation', 'One navbar per page, owned by the shared Frame.',
R('one-bar', 'navigation', 'One navbar per page, owned by the shared Frame.',
'A site’s own section links go in the page body, never in a second header under the bar.',
'A site’s own section links go in the page body, never in a second header under the bar.',
'2026-10-30', 'Page headers under the bar doubled the navigation.',
'2026-10-30', 'Page headers under the bar doubled the navigation.',
@@ -25,6 +25,16 @@ const { patterns, TOKENS, nativeSpecs, esc } = require('../server/patterns/catal
@@ -25,6 +25,16 @@ const { patterns, TOKENS, nativeSpecs, esc } = require('../server/patterns/catal
assert.equal(res.status, 200);
assert.equal(res.status, 200);
for (const name of ['Ribbon', 'Breadcrumb', 'Porthole', 'Liquid', 'Seal']) assert.ok(res.text.includes(name), name);
for (const name of ['Ribbon', 'Breadcrumb', 'Porthole', 'Liquid', 'Seal']) assert.ok(res.text.includes(name), name);
});
});
+ await check('the one-menu pattern draws the menu button at the top of the page, not the old burger', async () => {
+ const res = await t.get('/patterns/one-menu');
+ assert.equal(res.status, 200);
+ assert.ok(res.text.includes('The menu button at the top of the page'), 'the rule names the button');
+ assert.ok(!/burger/i.test(res.text), 'no burger in the rule');
+ const js = await t.get('/js/patterns.js');
+ assert.ok(js.text.includes('window.OpenVibeOrb ? window.OpenVibeOrb.svg(20, open)'), 'drawn by the shared orb');
+ assert.ok(js.text.includes('x="4" y="5.2" width="16" height="5.6"'), 'the inline fallback is the same shapes');
+ assert.ok(!js.text.includes('☰'), 'no three-line button');
+ });
await check('the live demo assets are served', async () => {
await check('the live demo assets are served', async () => {
for (const asset of ['/js/pattern-kit.js', '/js/patterns.js', '/css/patterns.css']) {
for (const asset of ['/js/pattern-kit.js', '/js/patterns.js', '/css/patterns.css']) {
const res = await t.get(asset);
const res = await t.get(asset);