openvibers/openvibe.shared Commits

4.237.0: You is one slim row, your site's own are apps, the menu's badges and the composer's row line up (#520)

OP OpenVibers committed 1 h ago · c6d2cf01fd · 15 files, +176 −87

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex e1f783d..96af2f4 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,30 @@
# Changelog
++## 4.237.0 — 2026-11-13
++
++**You is one slim row, your site's own are apps, and the menu's badges and the composer's row line up** (the owner's
++phone, 11-13: "completely redesign this card … the buttons are below", "it can be … an app … for admin, mod rank",
++"the red dot … next to the word feed", "improve … match" the badges, the composer's row "offset").
++
++- **You (ov-home.js).**
++ - Your card is one slim row: your picture with its online dot, your name and handle, Display, and your account's
++ menu (a chevron).
++ - The menu is anchored in the top layer (OVSurfaces.anchor) and holds Your profile, Account and settings, Add or
++ switch account and Sign out. The button that opens it closes it; Escape and a press elsewhere do too.
++ - What the site gives you (your channel, Admin, moderation: `me.own`) are apps at the start of the Apps tab, by
++ rank, each with a tooltip; they are no longer buttons in the card. They are their own row (`.ovh-own-apps`), never
++ part of the grid you arrange.
++- **The menu's tabs (search.js).**
++ - Feed's live dot sits after the word and breathes.
++ - Counts (Feed's new, You's unread) ride on the icon's corner instead of a third line under the label.
++ - Every count in the menu (the tabs' and You's Notifications tab) is one badge: an 18 px pill, a ring of the
++ ground, a soft shadow, and a pop-in.
++- **The composer's row.**
++ - Every tool is the same quiet 40 px disc, so the first one lines up with the words above it.
++ - An open + (or a pressed tool) is that disc tinted with a ring, not the only solid shape in the row.
++- Tests: `menu-home-screen-chrome` (the slim row, the account menu, own apps; `OV_CAPTURE_YOU` saves the open menu),
++ `menu-places-chrome` and `navbar-ask-vibe` updated.
++
## 4.236.0 — 2026-11-13
**You is yours on every site, the wait is the logo, and the box says what a message answers** (the owner's phone,
ddiff --git a/README.md b/README.md
iindex 0250f0e..9a1bcca 100644
---- a/README.md
++++ b/README.md
@@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Install and test:** `npm ci && npm test`
--**Status:** alpha, v4.236.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.237.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex f6cdcaf..2eada87 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.236.0",
++ "version": "4.237.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
ddiff --git a/dock-vibe.js b/dock-vibe.js
iindex b96b501..3e1c048 100644
---- a/dock-vibe.js
++++ b/dock-vibe.js
@@@ -114,7 +114,7 @@
// 4.203.0 (ask 349, "a better composer system unified across all sites"): Vibe's box is the one composer too, for
// drafts, "/" commands and your own, the marks, what sends and your settings; its picture keeps its own upload.
const selfSrc = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';
-- const VIBE_REV = { 'ov-composer.js': '021d6850b659' };
++ const VIBE_REV = { 'ov-composer.js': 'fbc3cc30fd2e' };
function joinComposer(form) {
if (!form || form.ownerDocument !== document) return; // a popped-out Dock keeps its plain box
form.setAttribute('data-ovc-no-files', '');
ddiff --git a/dock.js b/dock.js
iindex 858362f..b9072a0 100644
---- a/dock.js
++++ b/dock.js
@@@ -84,7 +84,7 @@
}
// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
-- const DOCK_REV = { 'dock-vibe.js': '7746fc647b45', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': 'ee1f6f21dbba', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '6cf8c6b6daa5' };
++ const DOCK_REV = { 'dock-vibe.js': '39bffcfd6f09', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': 'ee1f6f21dbba', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '6cf8c6b6daa5' };
function load(src, test) {
return new Promise((ok) => {
if (test()) return ok(true);
ddiff --git a/navbar.js b/navbar.js
iindex bc2bdfd..2ad8b85 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -609,7 +609,7 @@
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
*/
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
-- const SIBLING_REV = { 'ov-mark.js': 'f490466f57df', 'ov-glyphs.js': '2ee871bd8ffa', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '95fb20ad04f9', 'dock.js': 'ea0b3c128ed0', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': '3e6f2784c85f', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'b2a16f53eae0', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8701ebc6f8a3', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'e8768e165d42', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '51840ff5c0b9', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': '021d6850b659' };
++ const SIBLING_REV = { 'ov-mark.js': 'f490466f57df', 'ov-glyphs.js': '2ee871bd8ffa', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '14fc6ffd84b8', 'dock.js': 'a17fcbc5fe4c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': '3e6f2784c85f', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'b2a16f53eae0', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '8701ebc6f8a3', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'a36477cdd6e4', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '51840ff5c0b9', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': 'fbc3cc30fd2e' };
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
// ov-tip.js loads at the first hover over a tooltip's owner.
ddiff --git a/ov-composer.js b/ov-composer.js
iindex 5489e29..76e6734 100644
---- a/ov-composer.js
++++ b/ov-composer.js
@@@ -324,6 +324,14 @@ html[data-ov-motion=off] .ovc-emoji { animation: none; }
.ovc-more[aria-expanded="true"] { background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 18%, transparent); }
.ovc-more svg { transition: transform .25s cubic-bezier(.3, 1.4, .4, 1); }
.ovc-more[aria-expanded="true"] svg { transform: rotate(45deg); }
++/* 4.237.0 (the owner, 11-13: the row looked "offset"): every tool in the row is the same quiet disc, so the first one's
++ edge lines up with the words above it and an open + is that disc tinted, not the only solid shape in the row; Send
++ is the same size at the other end. */
++.ovc-v2 .ovt-c-tools { gap: 6px; }
++.ovc-v2 .ovt-c-tools > .ovt-btn { background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }
++.ovc-v2 .ovt-c-tools > .ovt-btn:hover, .ovc-v2 .ovt-c-tools > .ovt-btn:focus-visible { background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 16%, transparent); }
++.ovc-v2 .ovt-c-tools > .ovc-more[aria-expanded="true"], .ovc-v2 .ovt-c-tools > .ovt-btn[aria-pressed="true"] { background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 22%, transparent); box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 45%, transparent); }
++.ovc-v2 .ovt-c-tools > .ovt-btn svg { width: 20px; height: 20px; }
@container ovc (max-width: 460px) {
.ovt-c-tools > .ovt-btn:not(.ovc-mic):not(.ovc-more) ~ .ovt-btn:not(.ovc-mic):not(.ovc-more):not([aria-pressed="true"]) { display: none; }
}
ddiff --git a/ov-home.js b/ov-home.js
iindex 238a9d0..d21ca90 100644
---- a/ov-home.js
++++ b/ov-home.js
@@@ -63,7 +63,7 @@
its glyph is about half the tile, folders scale their four with it, and the grid lines up with the cards above. */
.ovh-grid:not(.ovh-recent) { gap: 4px 2px; padding: 2px 0 0; justify-items: stretch; }
.ovh-grid:not(.ovh-recent) .ovh-app { min-height: 0; padding: 6px 0 8px; gap: 7px; }
--.ovh-grid:not(.ovh-recent) .ovh-ic { width: min(100% - 12px, 72px); height: auto; aspect-ratio: 1; border-radius: 29%; }
++.ovh-grid:not(.ovh-recent) .ovh-ic, .ovh-own-apps .ovh-ic { width: min(100% - 12px, 72px); height: auto; aspect-ratio: 1; border-radius: 29%; }
.ovh-ic > .ov-icon { --ovi-size: 74.4%; display: grid; place-items: center; }
.ovh-ic > .ov-icon svg { width: 100%; height: 100%; }
.ovh-grid:not(.ovh-recent) .ovh-folder .ovh-ic { grid-template-columns: repeat(2, 36%); grid-auto-rows: 36%; gap: 7%; place-content: center; }
@@@ -87,7 +87,7 @@
box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.1)); transition: transform .18s cubic-bezier(.2,1.4,.4,1); }
.ovh-recent .ovh-ic { width: 48px; height: 48px; border-radius: 15px; }
/* App icons: the ring goes, the glyph fills the tile, each app keeps its own hue (11-09). */
--.ovh-grid .ovh-ic, .ovh-recent .ovh-ic, .ovh-card .ovh-ic, .ovh-key .ovh-ic { background: linear-gradient(150deg, hsl(var(--ovh-h, 215) 72% 58% / .34), hsl(var(--ovh-h, 215) 70% 38% / .18)); box-shadow: inset 0 0 0 1px hsl(var(--ovh-h, 215) 70% 64% / .28), 0 4px 14px hsl(var(--ovh-h, 215) 70% 30% / .18); overflow: hidden; }
++.ovh-grid .ovh-ic, .ovh-own-apps .ovh-ic, .ovh-recent .ovh-ic, .ovh-card .ovh-ic, .ovh-key .ovh-ic { background: linear-gradient(150deg, hsl(var(--ovh-h, 215) 72% 58% / .34), hsl(var(--ovh-h, 215) 70% 38% / .18)); box-shadow: inset 0 0 0 1px hsl(var(--ovh-h, 215) 70% 64% / .28), 0 4px 14px hsl(var(--ovh-h, 215) 70% 30% / .18); overflow: hidden; }
.ovh-ic .ovi-ring, .ovh-ic .ovi-bg, .ovh-ic .ovi-comet, .ovh-ic .ovi-prog { display: none; }
.ovh-ic .ov-icon { --ovi-glyph: hsl(var(--ovh-h, 215) 90% 82%); color: hsl(var(--ovh-h, 215) 90% 82%); }
:root[data-theme="light"] .ovh-ic .ov-icon { --ovi-glyph: hsl(var(--ovh-h, 215) 70% 32%); color: hsl(var(--ovh-h, 215) 70% 32%); }
@@@ -266,7 +266,9 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
.ovh-tab:focus-visible { outline: 2px solid var(--accent-light, var(--accent, #60a5fa)); outline-offset: -2px; }
.ovh-tab[aria-selected="true"] { color: var(--text-primary, #e6edf7); background: var(--bg-secondary, #121826); box-shadow: 0 1px 2px rgba(0,0,0,.3), inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 30%, transparent); }
.ovh-tab[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); }
--.ovh-tab-n { min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box; border-radius: 999px; background: var(--danger, #ef4444); color: #fff; font: 750 11px/18px inherit; text-align: center; }
++.ovh-tab-n { min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box; border-radius: 999px; font: 750 11px/18px Inter, system-ui, sans-serif; letter-spacing: -.01em; text-align: center; font-variant-numeric: tabular-nums; color: #fff; box-shadow: 0 0 0 2px var(--bg-secondary, #141a24), 0 3px 8px -2px rgba(0, 0, 0, .55); animation: ovhBadgeIn .3s cubic-bezier(.34, 1.56, .64, 1); background: var(--danger, #ef4444); }
++@keyframes ovhBadgeIn { from { transform: scale(.5); opacity: 0; } }
++html[data-ov-motion=off] .ovh-tab-n { animation: none; }
@media (max-width: 380px) { .ovh-tab { gap: 5px; font-size: 13px; } .ovh-tab svg { width: 16px; height: 16px; } }
.ovh-notes { min-height: 120px; }
.ovh-empty { display: flex; align-items: center; gap: 14px; margin: 6px 0; padding: 14px 16px; border-radius: 14px; border: 1px dashed color-mix(in srgb, var(--text-muted, #93a0b4) 45%, transparent); }
@@@ -298,33 +300,30 @@ html[data-ov-motion=reduced] .ovh-card { animation-name: ovh-fade; animation-dur
/* Your row: you, then your round buttons (this site's own first). On a narrow screen the buttons go under your name. */
.ovh-me { flex-wrap: wrap; row-gap: 0; padding: 6px 6px 6px 8px; min-height: 56px; box-sizing: border-box; }
.ovh-acts { display: flex; align-items: center; gap: 4px; flex: none; }
--/* 4.236.0, your card: your name and the gear on one line, then one row of actions across the card, each its glyph with
-- its name under it in equal columns (a tooltip says the whole of it); Sign out last, its red only under a pointer. */
--.ovh-mebox { margin: 2px 4px 8px; padding: 8px; border-radius: 18px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); }
--.ovh-mebox > .ovh-me { margin: 0; padding: 0 0 0 4px; min-height: 48px; background: none; box-shadow: none; flex-wrap: nowrap; }
--.ovh-mebox .ovh-av { width: 42px; height: 42px; }
--.ovh-me-gear { margin-left: auto; color: var(--text-secondary, #a8b3c4); }
--.ovh-me-gear svg { transition: transform .5s cubic-bezier(.3, 1.4, .4, 1); }
--@media (hover: hover) and (pointer: fine) { .ovh-me-gear:hover svg { transform: rotate(60deg); } }
--.ovh-mebox > .ovh-acts { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 6px; padding: 8px 0 0; margin: 8px 0 0; border-top: 1px solid color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); }
--.ovh-mebox > .ovh-acts > .ovh-act { margin: 0; }
--.ovh-mebox .ovh-act { align-self: stretch; min-width: 0; width: auto; height: auto; min-height: 56px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 6px 4px; border-radius: 14px;
-- background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); color: var(--text-primary, #e6edf7);
-- transition: background-color .18s, color .18s, transform .18s cubic-bezier(.3, 1.4, .4, 1); }
--.ovh-mebox .ovh-act svg { width: 20px; height: 20px; transition: transform .25s cubic-bezier(.3, 1.4, .4, 1); }
--.ovh-act-t { font-size: 11.5px; font-weight: 700; line-height: 1.1; color: var(--text-secondary, #a8b3c4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
--.ovh-act-ab { font-size: 13px; font-weight: 800; line-height: 20px; }
--.ovh-mebox .ovh-act.is-own { color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 13%, transparent); }
--.ovh-mebox .ovh-act.is-own .ovh-act-t { color: inherit; }
--@media (hover: hover) and (pointer: fine) {
-- .ovh-mebox .ovh-act:hover { background: color-mix(in srgb, var(--text-primary, #e6edf7) 10%, transparent); transform: translateY(-1px); }
-- .ovh-mebox .ovh-act:hover svg { transform: translateY(-1px) scale(1.06); }
-- .ovh-mebox .ovh-out:hover { color: var(--danger, #f87171); background: color-mix(in srgb, var(--danger, #ef4444) 12%, transparent); }
--}
--.ovh-mebox .ovh-act:active { transform: scale(.94); }
--.ovh-mebox .ovh-act[aria-expanded="true"] { background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff); }
--.ovh-mebox .ovh-act[aria-expanded="true"] .ovh-act-t { color: inherit; }
--html[data-ov-motion=off] .ovh-mebox .ovh-act, html[data-ov-motion=off] .ovh-mebox .ovh-act svg, html[data-ov-motion=off] .ovh-me-gear svg { transition: none; }
++/* 4.237.0, you: one slim row (your picture with its online dot, your name, Display, your account's menu); the menu sits
++ in the top layer; what the site gives you are apps at the start of the grid. */
++.ovh-mebox { margin: 2px 4px 8px; }
++.ovh-mebox > .ovh-me { display: flex; align-items: center; gap: 6px; margin: 0; padding: 6px 6px 6px 8px; min-height: 0; border-radius: 18px; flex-wrap: nowrap;
++ background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); }
++.ovh-mebox .ovh-who { flex: 1 1 auto; min-width: 0; min-height: 44px; gap: 11px; padding: 2px 6px 2px 2px; border-radius: 14px; transition: background-color .15s; }
++@media (hover: hover) and (pointer: fine) { .ovh-mebox .ovh-who:hover { background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); } }
++.ovh-mebox .ovh-av { width: 40px; height: 40px; }
++.ovh-mebox .ovh-who-txt b { font-size: 15.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal; }
++.ovh-mebox .ovh-who-txt small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal; }
++.ovh-mebox .ovh-me .ovh-ib { flex: none; width: 42px; height: 42px; border-radius: 13px; color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }
++.ovh-mebox .ovh-acct svg { transition: transform .25s cubic-bezier(.3, 1.4, .4, 1); }
++.ovh-mebox .ovh-acct[aria-expanded="true"] { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); }
++.ovh-mebox .ovh-acct[aria-expanded="true"] svg { transform: rotate(180deg); }
++.ovh-acct-menu { min-width: 230px; padding: 6px; border-radius: 16px; background: var(--bg-secondary, #141a24); color: var(--text-primary, #e6edf7);
++ box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-primary, #e6edf7) 10%, transparent), 0 18px 40px -12px rgba(0, 0, 0, .7); }
++.ovh-acct-i { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 12px; width: 100%; min-height: 44px; padding: 0 12px; border-radius: 11px; cursor: pointer; font-size: 14.5px; font-weight: 650; }
++.ovh-acct-i svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; color: var(--text-secondary, #a8b3c4); }
++.ovh-acct-i:hover, .ovh-acct-i:focus-visible { background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); }
++.ovh-acct-i.is-danger, .ovh-acct-i.is-danger svg { color: var(--danger, #f87171); }
++.ovh-own-apps { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 2px; padding: 2px; margin-bottom: 4px; padding-bottom: 10px; border-bottom: 1px solid color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); }
++.ovh-own-ic { color: hsl(var(--ovh-h, 220) 80% 72%); }
++.ovh-own-ic svg { width: 46%; height: 46%; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
++html[data-ov-motion=off] .ovh-mebox .ovh-acct svg { transition: none; }
.ovh-ib.is-own { color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 13%, transparent); }
.ovh-ib.is-text { width: auto; padding: 0 12px; border-radius: 999px; font-size: 12.5px; font-weight: 750; white-space: nowrap; }
@container ovh (max-width: 520px) {
@@@ -370,8 +369,8 @@ html[data-ov-motion=off] .ovh-headtools .ovh-edit.is-done { animation: none; }
@media (hover: hover) { .ovh-back:hover { background: color-mix(in srgb, var(--text-primary, #e8ecf3) 15%, transparent); } }
.ovh-head .ovh-filter { flex: 1; min-width: 0; width: auto; margin: 0; }
/* The grid: four across on a phone, as many as fit from 560 px. */
--.ovh-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
--@media (min-width: 560px) { .ovh-grid { grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); } }
++.ovh-grid, .ovh-own-apps { grid-template-columns: repeat(4, minmax(0, 1fr)); }
++@media (min-width: 560px) { .ovh-grid, .ovh-own-apps { grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); } }
/* App icons: the glyph at 58% of the tile. */
.ovh-ic > .ov-icon { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.ovh-ic > .ov-icon svg { position: absolute; left: -13%; top: -13%; width: 126%; height: 126%; margin: 0; transform: none !important; }
@@@ -592,6 +591,7 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
const ICON = {
pencil: [['path', { d: 'M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16z' }], ['path', { d: 'M13.5 6.5l4 4' }]],
check: [['path', { d: 'M5 12.5l4.5 4.5L19 7.5' }]],
++ chev: [['path', { d: 'M6.5 9.5l5.5 5.5 5.5-5.5' }]],
briefcase: [['rect', { x: 3.5, y: 7.2, width: 17, height: 12.3, rx: 2.4 }], ['path', { d: 'M8.8 7.2V5.6a1.8 1.8 0 0 1 1.8-1.8h2.8a1.8 1.8 0 0 1 1.8 1.8v1.6M3.5 12.6h17' }]],
bell: [['path', { d: 'M6 14.6V9.5a6.1 6.1 0 0 1 12.2 0v5.1l1.5 2.1h-15.3Z' }], ['path', { d: 'M10 18.7a2.1 2.1 0 0 0 4 0' }]],
history: [['path', { d: 'M3.8 12.1a8.3 8.3 0 1 0 2.5-5.9L3.8 8.5M3.8 4.2v4.2h4.2' }], ['path', { d: 'M12 7.9v4.3l3.1 1.9' }]],
@@@ -710,49 +710,88 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
// Your row (4.195.0): the old account block is not set here any more (the owner: "get rid of this thing … we
// didn't really need [it] here, other than like adding your account"). The row has the five things a person needs of
// an account; under it, only what has no other home in the menu.
++ /**
++ * You (4.237.0; the owner, 11-13: "completely redesign this card … I hate that thing … the buttons are below"):
++ * one slim row, your picture, your name and that you are online, Display, and your account's menu. The menu
++ * (anchored in the top layer, OVSurfaces) holds your profile, settings, switching account and signing out. What
++ * this site gives you as staff or as a creator (your channel, Admin, moderation) are apps at the start of the
++ * grid, not buttons here (ownApps below).
++ */
function me() {
if (meBox) return meBox;
const u = o.user || {};
-- meBox = h('div');
++ const m = o.me || {};
++ meBox = h('div', 'ovh-mebox');
const bar = h('div', 'ovh-me');
-- const who = h('a', 'ovh-who'); who.href = u.username ? `https://openvibe.network/@${encodeURIComponent(u.username)}` : 'https://openvibe.network/my';
++ const profile = u.username ? `https://openvibe.network/@${encodeURIComponent(u.username)}` : 'https://openvibe.network/my';
++ const who = h('a', 'ovh-who'); who.href = profile; who.setAttribute('data-ov-tip', 'Your profile');
const av = h('span', 'ovh-av');
const pic = /^https:\/\//.test(String(u.avatar_url || '')) ? u.avatar_url : '';
if (pic) { const img = h('img'); img.alt = ''; img.src = pic; img.decoding = 'async'; av.append(img); } else av.textContent = String(u.display_name || u.username || '?').trim().charAt(0).toUpperCase();
const txt = h('span', 'ovh-who-txt');
txt.append(h('b', '', u.display_name || u.username || 'You'), h('small', '', [u.username ? `@${u.username}` : '', 'Online'].filter(Boolean).join(' · ')));
who.append(av, txt);
-- const look = h('a', 'ovh-ib'); look.append(svgIcon('look')); look.href = 'https://openvibe.design/look'; look.setAttribute('data-ov-look-open', '');
-- look.setAttribute('aria-expanded', 'false'); look.setAttribute('aria-label', 'Display: theme, text size, motion'); look.title = 'Display';
-- const gear = h('a', 'ovh-ib ovh-me-gear'); gear.append(svgIcon('gear')); gear.href = 'https://openvibe.network/my'; gear.setAttribute('aria-label', 'Account and settings'); gear.setAttribute('data-ov-tip', 'Account and settings');
-- // 4.236.0 (the owner, 11-13: "completely redesign this area … confusing what these buttons even do … not
-- // perfectly aligned … tooltips … a waste of space in the corners"): your name and the gear on one line; under
-- // it one row of actions across the whole card, each its glyph with its name under it, in equal columns.
-- const named = (b, label, short) => { b.classList.add('ovh-act'); b.append(h('span', 'ovh-act-t', short || label)); b.setAttribute('aria-label', label); b.setAttribute('data-ov-tip', label); return b; };
-- bar.append(who, gear);
-- const m = o.me || {};
-- const acts = h('div', 'ovh-acts'); acts.setAttribute('role', 'group'); acts.setAttribute('aria-label', 'Yours');
-- // This site's own items (your channel, Admin, what a site adds) as round buttons beside Display and Settings:
-- // a known one with its glyph, a site's own with its short name (4.223.0; it was a row of its own).
-- const OWN_GLYPH = { 'ov-channel': 'live', 'ov-admin': 'shield' };
-- const own = (Array.isArray(m.own) ? m.own : []).filter((it) => it && it.label && (typeof it.onClick === 'function' || (it.href && safe(it.href)))).slice(0, 6);
-- own.forEach((it) => {
-- const g = OWN_GLYPH[it.id] || (ICON[it.glyph] ? it.glyph : '');
-- const b = it.href ? h('a', `ovh-ib is-own${g ? '' : ' is-text'}`) : h('button', `ovh-ib is-own${g ? '' : ' is-text'}`);
-- if (it.href) b.href = it.href; else { b.type = 'button'; b.addEventListener('click', () => { try { it.onClick(); } catch { /* the item is the site's */ } }); }
-- b.append(g ? svgIcon(g) : h('b', 'ovh-act-ab', String(it.short || it.label).slice(0, 2)));
-- acts.append(named(b, it.label, it.short || (it.id === 'ov-channel' ? 'Channel' : it.id === 'ov-admin' ? 'Admin' : '')));
++ const look = h('a', 'ovh-ib ovh-me-look-btn'); look.append(svgIcon('look')); look.href = 'https://openvibe.design/look'; look.setAttribute('data-ov-look-open', '');
++ look.setAttribute('aria-expanded', 'false'); look.setAttribute('aria-label', 'Display: theme, text size, motion'); look.setAttribute('data-ov-tip', 'Display');
++ const acct = h('button', 'ovh-ib ovh-acct'); acct.type = 'button'; acct.append(svgIcon('chev'));
++ acct.setAttribute('aria-haspopup', 'menu'); acct.setAttribute('aria-expanded', 'false');
++ acct.setAttribute('aria-label', 'Your account'); acct.setAttribute('data-ov-tip', 'Your account');
++ bar.append(who, look, acct);
++
++ const menu = h('div', 'ovh-acct-menu'); menu.setAttribute('role', 'menu'); menu.setAttribute('aria-label', 'Your account'); menu.hidden = true;
++ const item = (glyph, label, act, danger) => {
++ const it = typeof act === 'string' ? h('a', 'ovh-acct-i') : h('button', 'ovh-acct-i');
++ if (typeof act === 'string') it.href = act; else { it.type = 'button'; it.addEventListener('click', () => { close(); try { act(); } catch { /* the site's */ } }); }
++ if (danger) it.classList.add('is-danger');
++ it.setAttribute('role', 'menuitem');
++ it.append(svgIcon(glyph), h('span', '', label));
++ menu.append(it);
++ };
++ item('user', 'Your profile', profile);
++ item('gear', 'Account and settings', 'https://openvibe.network/my');
++ if (m.add && safe(m.add)) item('plus', 'Add or switch account', m.add);
++ if (typeof m.out === 'function') item('out', 'Sign out', () => m.out(), true);
++ let anchored = null;
++ const onDown = (e) => { if (!menu.contains(e.target) && !acct.contains(e.target)) close(); };
++ const onKey = (e) => { if (e.key === 'Escape') { close(); acct.focus(); } };
++ function close() {
++ if (menu.hidden) return;
++ menu.hidden = true; acct.setAttribute('aria-expanded', 'false');
++ if (anchored) { anchored.release(); anchored = null; }
++ document.removeEventListener('pointerdown', onDown, true); document.removeEventListener('keydown', onKey, true);
++ }
++ acct.addEventListener('click', () => {
++ if (!menu.hidden) { close(); return; }
++ menu.hidden = false; acct.setAttribute('aria-expanded', 'true');
++ const S = typeof window !== 'undefined' ? window.OVSurfaces : null;
++ if (S && typeof S.anchor === 'function') anchored = S.anchor(menu, acct, { place: 'bottom-end', sheet: false });
++ document.addEventListener('pointerdown', onDown, true); document.addEventListener('keydown', onKey, true);
++ const first = menu.querySelector('.ovh-acct-i'); if (first) first.focus({ preventScroll: true });
});
-- look.classList.remove('ovh-ib'); look.classList.add('ovh-ib'); named(look, 'Display: theme, text size, motion', 'Display'); look.removeAttribute('title');
-- acts.append(look);
-- if (m.add && safe(m.add)) { const add = h('a', 'ovh-ib'); add.append(svgIcon('plus')); add.href = m.add; acts.append(named(add, 'Add or switch account', 'Switch')); }
-- if (typeof m.out === 'function') { const out = h('button', 'ovh-ib ovh-out'); out.type = 'button'; out.append(svgIcon('out')); named(out, 'Sign out'); out.addEventListener('click', () => { try { m.out(); } catch { /* the bar's own */ } }); acts.append(out); }
const panel = h('div', 'ud-look ovh-me-look'); panel.hidden = true;
-- meBox.className = 'ovh-mebox';
-- meBox.append(bar, acts, panel);
++ meBox.append(bar, menu, panel);
return meBox;
}
++ /** What this site gives you (your channel, Admin, moderation): apps at the start of the grid, by your rank. */
++ function ownApps() {
++ const m = o.me || {};
++ const OWN = { 'ov-channel': ['live', 'Your channel'], 'ov-admin': ['shield', 'Admin'], 'ov-mod': ['shield', 'Moderation'] };
++ const own = (Array.isArray(m.own) ? m.own : []).filter((it) => it && it.label && (typeof it.onClick === 'function' || (it.href && safe(it.href)))).slice(0, 8);
++ if (!own.length) return null;
++ const row = h('div', 'ovh-own-apps'); row.setAttribute('aria-label', 'Yours on this site');
++ own.forEach((it) => {
++ const [g, name] = OWN[it.id] || [ICON[it.glyph] ? it.glyph : 'star', it.short || it.label];
++ const el = it.href ? h('a', 'ovh-app ovh-own-app') : h('button', 'ovh-app ovh-own-app');
++ if (it.href) el.href = it.href; else { el.type = 'button'; el.addEventListener('click', () => { try { it.onClick(); } catch { /* the site's */ } }); }
++ el.setAttribute('data-ov-tip', it.label);
++ const ic = h('span', 'ovh-ic ovh-own-ic'); ic.style.setProperty('--ovh-h', String(hueOf(it.id))); ic.append(svgIcon(g));
++ el.append(ic, h('span', 'ovh-name', name));
++ row.append(el);
++ });
++ return row;
++ }
++
/** The three tabs under your row; Notifications carries how many wait. */
let countBound = false, inkWas = null, tabWas = null;
function youTabs() {
@@@ -966,6 +1005,7 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
head.append(tools);
el.append(head);
}
++ if (known(o)) { const own = ownApps(); if (own) el.append(calm(own)); }
const grid = h('div', 'ovh-grid'); grid.setAttribute('aria-label', 'Your home');
L.home.forEach((id, i) => grid.append(homeTile(id, i)));
el.append(grid);
ddiff --git a/ov-thread.js b/ov-thread.js
iindex aed0bb9..ddc0491 100644
---- a/ov-thread.js
++++ b/ov-thread.js
@@@ -24,7 +24,7 @@
// Motion off (html[data-ov-motion=off], theme-loader.js) is the only setting that skips an effect; Calm plays it.
var reduce = document.documentElement.getAttribute('data-ov-motion') === 'off';
var canCopy = !!(root.navigator && navigator.clipboard && navigator.clipboard.writeText);
-- const THREAD_REV = { 'ov-composer.js': '021d6850b659', 'ov-combo.js': '8001a3b19c2f' };
++ const THREAD_REV = { 'ov-composer.js': 'fbc3cc30fd2e', 'ov-combo.js': '8001a3b19c2f' };
var selfSrc = (document.currentScript && document.currentScript.src) || '';
// 4.202.0: the one dropdown for a field (ov-combo.js: a repository, a person), wherever a page asks for it.
function loadCombo() {
ddiff --git a/package-lock.json b/package-lock.json
iindex 4befc09..ffbd482 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.236.0",
++ "version": "4.237.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.236.0",
++ "version": "4.237.0",
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
ddiff --git a/package.json b/package.json
iindex 8ce95c1..5e184fc 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.236.0",
++ "version": "4.237.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/search.js b/search.js
iindex 042ed78..294741b 100644
---- a/search.js
++++ b/search.js
@@@ -82,7 +82,7 @@
const b = tabsEl.querySelector('[data-tab="feed"]'); if (!b) return;
const old = b.querySelector('.ovs-tabnew'); if (old) old.remove();
const n = Number(count) || 0;
-- if (n) { const badge = document.createElement('b'); badge.className = 'ovs-tabnew'; badge.textContent = n > 9 ? '9+' : String(n); badge.setAttribute('aria-label', `${n} new since you last looked`); b.appendChild(badge); }
++ if (n) { const badge = document.createElement('b'); badge.className = 'ovs-tabnew'; badge.textContent = n > 9 ? '9+' : String(n); badge.setAttribute('aria-label', `${n} new since you last looked`); (b.querySelector('.ovs-tab-ic') || b).appendChild(badge); }
}
let feedChannel;
try { if (typeof BroadcastChannel === 'function') { feedChannel = new BroadcastChannel('openvibe-feed-seen'); feedChannel.onmessage = (e) => { const n = Number(e.data && e.data.count) || 0; paintFeedDot(n); paintFeedTabCount(n); }; } } catch { feedChannel = null; }
@@@ -297,11 +297,19 @@ html[data-ov-motion=off] .ovs-tab .ovs-roll { animation: none; }
.ovs-tab-av { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; display: block; box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--text-primary, #e6edf7) 18%, transparent); }
.ovs-tab[aria-selected="true"] .ovs-tab-av { box-shadow: 0 0 0 2px var(--accent, #3b82f6); }
.ovs-tab-on { position: absolute; right: -2px; bottom: -2px; width: 8px; height: 8px; border-radius: 50%; background: var(--success, #22c55e); box-shadow: 0 0 0 2px var(--bg-secondary, #111826); }
--.ovs-tabnew { flex: none; min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box; border-radius: 999px; background: var(--accent, #3b82f6); color: var(--on-accent, #fff); font: 700 11px/18px Inter, system-ui, sans-serif; text-align: center; }
++.ovs-tabnew { flex: none; min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box; border-radius: 999px; font: 750 11px/18px Inter, system-ui, sans-serif; letter-spacing: -.01em; text-align: center; font-variant-numeric: tabular-nums; color: #fff; box-shadow: 0 0 0 2px var(--bg-secondary, #141a24), 0 3px 8px -2px rgba(0, 0, 0, .55); animation: ovsBadgeIn .3s cubic-bezier(.34, 1.56, .64, 1); background: var(--accent, #3b82f6); }
.ovs-tabnew-red { background: var(--danger, #ef4444); }
.openvibe-navbar .ovnav-orb .ovs-feed-dot { position: absolute; top: 2px; right: 2px; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-light, var(--accent, #60a5fa)); }
.openvibe-navbar .ovnav-orb .ovs-feed-dot[hidden] { display: none; }
.ovs-tabdot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--live-red, #ef4444); box-shadow: 0 0 0 3px color-mix(in srgb, var(--live-red, #ef4444) 22%, transparent); }
++/* 4.237.0: one badge for every count in the menu (the You tab's, Feed's, Notifications'), on the icon's corner; the live
++ dot after the word, breathing. */
++.ovs-tab-ic > .ovs-tabnew { position: absolute; top: -7px; left: calc(100% - 8px); z-index: 1; }
++.ovs-tab span > .ovs-tabdot { display: inline-block; margin: 0 0 1px 5px; vertical-align: middle; animation: ovsLiveDot 2s ease-in-out infinite; }
++.ovs-tab > span:not(.ovs-tab-ic) { display: inline-flex; align-items: center; }
++@keyframes ovsLiveDot { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--live-red, #ef4444) 10%, transparent); } }
++@keyframes ovsBadgeIn { from { transform: scale(.5); opacity: 0; } }
++html[data-ov-motion=off] .ovs-tab span > .ovs-tabdot, html[data-ov-motion=off] .ovs-tabnew { animation: none; }
.ovs-grid.is-mini { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2px; padding: 0 2px 4px; }
.ovs-grid.is-mini .ovs-tile { gap: 8px; min-height: 40px; padding: 4px 8px; border-radius: 10px; }
.ovs-grid.is-mini .ovs-ic { width: 26px; height: 26px; border-radius: 8px; }
@@@ -1391,11 +1399,12 @@ html[data-ov-motion=off] .ovs-panel::before { animation: none; }
} else if (TAB_SVG[id]) ic.innerHTML = TAB_SVG[id];
if (ic.firstChild) b.appendChild(ic);
const t = document.createElement('span'); t.textContent = id === 'here' ? here() : label; b.appendChild(t);
-- if (id === 'feed' && live) { const d = document.createElement('i'); d.className = 'ovs-tabdot'; d.setAttribute('aria-label', 'live now'); b.appendChild(d); }
-- else if (id === 'feed' && fresh) { const n = document.createElement('b'); n.className = 'ovs-tabnew'; n.textContent = fresh > 9 ? '9+' : String(fresh); n.setAttribute('aria-label', `${fresh} new since you last looked`); b.appendChild(n); }
++ // 4.237.0 (the owner, 11-13): the live dot after the word, a count on the icon's corner, never a third line.
++ if (id === 'feed' && live) { const d = document.createElement('i'); d.className = 'ovs-tabdot'; d.setAttribute('aria-label', 'live now'); t.appendChild(d); }
++ else if (id === 'feed' && fresh) { const n = document.createElement('b'); n.className = 'ovs-tabnew'; n.textContent = fresh > 9 ? '9+' : String(fresh); n.setAttribute('aria-label', `${fresh} new since you last looked`); (b.querySelector('.ovs-tab-ic') || b).appendChild(n); }
if (id === 'you' && ctx.token && root.OpenVibeNotifications) {
const count = Number(root.OpenVibeNotifications.unread ? root.OpenVibeNotifications.unread() : 0) || 0;
-- if (count) { const n = document.createElement('b'); n.className = 'ovs-tabnew ovs-tabnew-red'; n.textContent = count > 99 ? '99+' : String(count); n.setAttribute('aria-label', `${count} unread notifications`); b.appendChild(n); }
++ if (count) { const n = document.createElement('b'); n.className = 'ovs-tabnew ovs-tabnew-red'; n.textContent = count > 99 ? '99+' : String(count); n.setAttribute('aria-label', `${count} unread notifications`); (b.querySelector('.ovs-tab-ic') || b).appendChild(n); }
}
tabsEl.appendChild(b);
}
ddiff --git a/test/menu-home-screen-chrome.test.js b/test/menu-home-screen-chrome.test.js
iindex 7ec6deb..848cb08 100644
---- a/test/menu-home-screen-chrome.test.js
++++ b/test/menu-home-screen-chrome.test.js
@@@ -231,16 +231,23 @@ const server = http.createServer((req, res) => {
// button (this site's own first, as icons), one AI notice, then one row of tabs with All and Edit at its end.
await eventually(async () => assert.strictEqual(await s.evaluate(`document.querySelector('.ovh-me .ovh-who-txt b').textContent`), 'Ada', 'your name on top'));
await eventually(async () => assert.ok(await s.evaluate(`!document.querySelector('.ovs .ovh-account, .ovs .openvibe-navbar-dropdown, .ovs .ud-grid')`), 'the old account block and its grid of tiles are not in the menu'));
-- // 4.236.0 (the owner, 11-13: "completely redesign this area … confusing what these buttons even do … not perfectly
-- // aligned … tooltips … a waste of space in the corners"): your name and the gear on one line, then one row of
-- // actions across the whole card, each its glyph with its name, in equal columns; this site's own first.
-- await eventually(async () => assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-mebox > .ovh-acts > .ovh-act:not(.is-own)')].map((b) => b.getAttribute('aria-label'))`), ['Display: theme, text size, motion', 'Add or switch account', 'Sign out'], 'Display, add or switch account, Sign out, in that order'));
-- await eventually(async () => assert.ok(await s.evaluate(`[...document.querySelectorAll('.ovh-mebox .ovh-act')].every((b) => !!b.getAttribute('data-ov-tip') && !!b.querySelector('.ovh-act-t').textContent.trim())`), 'every action says its name and has a tooltip'));
-- await eventually(async () => assert.ok(await s.evaluate(`!document.querySelector('.ovh-own') && [...document.querySelectorAll('.ovh-mebox .ovh-acts > .is-own')].every((b) => !!b.getAttribute('aria-label'))`), 'this site\'s own items are in your row, never a row of their own'));
-- await eventually(async () => assert.ok(await s.evaluate(`/[?&]add_account=1/.test(document.querySelector('.ovh-mebox a.ovh-act[aria-label="Add or switch account"]').getAttribute('href')) && !!document.querySelector('.ovh-mebox [data-ov-look-open]') && !!document.querySelector('.ovh-me a.ovh-me-gear[href="https://openvibe.network/my"]')`), 'switch, Display, and the gear beside your name'));
-- await eventually(async () => assert.ok(await s.evaluate(`!document.querySelector('.ovs-displayrow')`), 'Display is not at the foot any more'));
-- await eventually(async () => assert.ok(await s.evaluate(`(() => { const acts = [...document.querySelectorAll('.ovh-mebox .ovh-act')].map((b) => b.getBoundingClientRect()); const box = document.querySelector('.ovh-mebox').getBoundingClientRect(); return acts.length > 0 && acts.every((r) => r.height >= 44 && r.width >= 44 && r.right <= box.right + 1 && r.left >= box.left - 1) && Math.abs(acts[0].left - (box.left + 8)) < 2; })()`), 'full touch targets, inside the card, the row starting at its edge (no indent)'));
-- await eventually(async () => { const t = await s.evaluate(`[...document.querySelectorAll('.ovh-mebox .ovh-act')].map((b) => { const r = b.getBoundingClientRect(); return [Math.round(r.top), Math.round(r.left), Math.round(r.width), b.className]; })`); assert.strictEqual(new Set(t.map((x) => x[0])).size, 1, `one row, aligned: ${JSON.stringify(t)}`); });
++ // 4.237.0 (the owner, 11-13: "completely redesign this card … the buttons are below"): one slim row (you, Display,
++ // your account's menu); the menu holds profile, settings, switch and Sign out; the site's own are apps in the grid.
++ await eventually(async () => assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-mebox > .ovh-me > .ovh-ib')].map((b) => b.getAttribute('aria-label'))`), ['Display: theme, text size, motion', 'Your account'], 'one row: Display and your account'));
++ await eventually(async () => assert.ok(await s.evaluate(`(() => { const r = document.querySelector('.ovh-mebox > .ovh-me').getBoundingClientRect(); return r.height <= 64 && r.right <= innerWidth; })()`), 'a slim row, inside the screen'));
++ await s.evaluate(`document.querySelector('.ovh-acct').click(), true`);
++ await eventually(async () => assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-acct-menu:not([hidden]) .ovh-acct-i')].map((i) => i.textContent)`), ['Your profile', 'Account and settings', 'Add or switch account', 'Sign out'], 'the account menu'));
++ assert.ok(/[?&]add_account=1/.test(await s.evaluate(`document.querySelector('.ovh-acct-menu a.ovh-acct-i:nth-child(3)').getAttribute('href')`)), 'switch carries add_account');
++ await s.evaluate(`document.querySelector('.ovh-acct').click(), true`);
++ await eventually(async () => assert.ok(await s.evaluate(`document.querySelector('.ovh-acct-menu').hidden`), 'the button that opens it closes it'));
++ await eventually(async () => assert.ok(await s.evaluate(`!document.querySelector('.ovs-displayrow') && [...document.querySelectorAll('.ovh-own-apps .ovh-own-app')].every((a) => !!a.getAttribute('data-ov-tip'))`), 'the site\'s own are apps with a tooltip'));
++ if (process.env.OV_CAPTURE_YOU) {
++ await s.evaluate(`document.querySelector('.ovh-acct').click(), true`);
++ await new Promise((ok) => setTimeout(ok, 250));
++ const m = await s.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
++ fs.writeFileSync(path.join(process.env.OV_CAPTURE_YOU, 'you-menu-390.png'), Buffer.from(m.data, 'base64'));
++ await s.evaluate(`document.querySelector('.ovh-acct').click(), true`);
++ }
// The tab row: Apps first and chosen; All and Edit at its end; the tabs move with the arrow keys.
await eventually(async () => assert.deepStrictEqual(await s.evaluate(`[...document.querySelectorAll('.ovh-tabrow .ovh-tab')].map((t) => t.dataset.tab + (t.getAttribute('aria-selected') === 'true' ? '*' : ''))`), ['apps*', 'notifications', 'recent'], 'Apps, Notifications, Recent; Apps first'));
await eventually(async () => assert.ok(await s.evaluate(`!!document.querySelector('.ovh-tabrow .ovh-headtools .ovh-allbtn') && !!document.querySelector('.ovh-tabrow .ovh-headtools .ovh-edit') && !document.querySelector('.ovh-sec .ovh-edit')`), 'All and Edit sit at the end of the tab row'));
@@@ -312,7 +319,7 @@ const server = http.createServer((req, res) => {
const shotPage = await h.openPage(browser, context, { width: 390, height: 844, block: blocked });
await shotPage.goto(`${base}/?signed=1`, { maxMs: 6000 });
await openTab(shotPage, 'you');
-- assert.ok(await until(shotPage, `!!document.querySelector('.ovh-mebox .ovh-act') && !!document.querySelector('.ovh-tabrow .ovh-tab')`));
++ assert.ok(await until(shotPage, `!!document.querySelector('.ovh-mebox .ovh-acct') && !!document.querySelector('.ovh-tabrow .ovh-tab')`));
const you = await shotPage.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
fs.writeFileSync(path.join(process.env.OV_CAPTURE_YOU, 'you-390.png'), Buffer.from(you.data, 'base64'));
await shotPage.evaluate(`document.querySelector('.ovh-edit').click(), true`);
ddiff --git a/test/menu-places-chrome.test.js b/test/menu-places-chrome.test.js
iindex 39cc08c..670a1e3 100644
---- a/test/menu-places-chrome.test.js
++++ b/test/menu-places-chrome.test.js
@@@ -25,7 +25,7 @@ const server = http.createServer((req, res) => {
const OPEN = (dock) => `OpenVibeSearch.open({ service: 'chat', scope: { label: 'Chat', page: '/search' }, links: [{ label: 'Rooms', href: '/rooms' }], frame: {}, token: ${dock ? "'t'" : 'null'}, from: document.body,
user: ${dock ? "{ id: 7, username: 'maya', display_name: 'Maya' }" : 'null'}, dock: ${dock ? '(id) => { window.went = id; }' : 'null'},
dockMore: (action) => { window.timerCalled = action; }, ask: (text) => { window.asked = text; }, section: 'notifications' })`;
--const ROW = `Array.from(document.querySelectorAll('.ovs-starttabs .ovs-tab')).map((t) => { const r = t.getBoundingClientRect(); return { label: t.textContent.trim(), chosen: t.getAttribute('aria-selected') === 'true', door: t.getAttribute('data-dock') || '', role: t.getAttribute('role'), top: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), right: Math.round(r.right) }; })`;
++const ROW = `Array.from(document.querySelectorAll('.ovs-starttabs .ovs-tab')).map((t) => { const r = t.getBoundingClientRect(); return { label: (t.querySelector(':scope > span:not(.ovs-tab-ic)') || t).textContent.trim(), chosen: t.getAttribute('aria-selected') === 'true', door: t.getAttribute('data-dock') || '', role: t.getAttribute('role'), top: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), right: Math.round(r.right) }; })`;
(async () => {
await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-places-'));
ddiff --git a/test/navbar-ask-vibe.test.js b/test/navbar-ask-vibe.test.js
iindex f355e24..9993ca5 100644
---- a/test/navbar-ask-vibe.test.js
++++ b/test/navbar-ask-vibe.test.js
@@@ -96,7 +96,7 @@ const server = http.createServer((req, res) => {
assert.strictEqual(u.searchParams.get('from'), null, 'an http test page is not named');
// The picture opens the menu on You, with your own row in it (4.195.0: the old account block is not set there).
const until = async (expr, ms = 6000) => { const end = Date.now() + ms; while (Date.now() < end) { if (await p.evaluate(expr)) return true; await new Promise((r) => setTimeout(r, 80)); } return false; };
-- assert.ok(await until(`!!document.querySelector('.ovs.is-open .ovh-mebox .ovh-out')`), "the picture opened the menu's You tab, with your row and Sign out in it");
++ assert.ok(await until(`!!document.querySelector('.ovs.is-open .ovh-mebox .ovh-acct')`), "the picture opened the menu's You tab, with your row and your account's menu");
assert.strictEqual(await p.evaluate(`!!document.querySelector('.ovs .openvibe-navbar-dropdown')`), false, 'and the old account block is not in the menu');
console.log('navbar-ask-vibe: browser ok');
} finally {