openvibers/openvibe.design Commits

One menu button: the patterns page and rule draw the new button (#249)

OP OpenVibers committed 3 days ago · e8e419ab4b · 4 files, +28 −9

Changes

ddiff --git a/public/js/patterns.js b/public/js/patterns.js
iindex bf99e82..2adc83b 100644
---- a/public/js/patterns.js
++++ b/public/js/patterns.js
@@@ -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'}.`); };
ddiff --git a/server/patterns/catalog.js b/server/patterns/catalog.js
iindex 1cc57c4..2549438 100644
---- a/server/patterns/catalog.js
++++ b/server/patterns/catalog.js
@@@ -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')],
ddiff --git a/server/rules/register.js b/server/rules/register.js
iindex dbc6e10..dc5ffae 100644
---- a/server/rules/register.js
++++ b/server/rules/register.js
@@@ -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.',
ddiff --git a/test/patterns.test.js b/test/patterns.test.js
iindex 63e6c0a..b96525f 100644
---- a/test/patterns.test.js
++++ b/test/patterns.test.js
@@@ -25,6 +25,16 @@ const { patterns, TOKENS, nativeSpecs, esc } = require('../server/patterns/catal
assert.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);