openvibers/openvibe.shared Commits
The menu's Home: a site's home has a place in "On <site>" (#352)
Changes
@@@ -2,6 +2,10 @@## Unreleased++- **The menu's Home.** A site whose links never name its home (Exchange: Models, Chat, Arena…) now gets Home first in++ "On <site>", and on the home page Home wears the highlight like every other page's link (owner, 11-09). A site that++ already names its home gets no second one.++- **Footer legal links are 44 px wide to a finger.** On a touch screen, Terms, Privacy, DMCA and the compact linksreached 36 px across (11-09 tap audit on six sites, `tools/tap-audit.js`); their hit area now extends 5 px each side(they sit 20 px apart, so the areas never meet).@@@ -539,7 +539,7 @@const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)// END generated// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.-- const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'b2e5ef9b3773', 'dock.js': '4dacf614797b', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075' };++ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'ad94f524fa45', 'dock.js': '4dacf614797b', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075' };// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.@@@ -965,6 +965,11 @@// This site's own cards (a drop zone on Download, go-live status on Live, a new task on Codes…).for (const c of cards) groups.push({ title: c.title || '', as: 'card', card: c, section: `card:${c.id}`, items: [] });const pages = (ctx.links || []).filter((l) => l && l.label && typeof l.href === 'string' && !l.hidden).map((l) => ({ title: l.label, url: safeUrl(l.href), icon: 'go', drawn: l.icon && typeof ctx.navIcon === 'function' ? ctx.navIcon(l.icon) : '', kind: 'page' })).filter((x) => x.url);++ // A site's links that never name its home get one first, so the home page has a place of its own in the menu++ // and wears the highlight there too (owner 11-09, on openvibe.exchange: "shouldn't there be a link to home that's++ // highlighted").++ const rootHere = (x) => { try { const u = new URL(x.url, root.location.href); return u.origin === root.location.origin && (u.pathname.replace(/\/+$/, '') || '/') === '/'; } catch { return false; } };++ if (pages.length && !pages.some(rootHere)) pages.unshift({ title: 'Home', url: '/', icon: 'go', drawn: typeof ctx.navIcon === 'function' ? ctx.navIcon('fa-house') : '', kind: 'page' });// The page the person is on wears the highlight (owner 11-08): the link whose path is this page's, or the longest one// this page sits under (/messages/abc under Messages); "/" only on the home itself.let best = -1, bestLen = -1;@@@ -19,13 +19,13 @@ if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {const FRAME = { nav: [{ id: 'chat', name: 'Chat', url: 'https://openvibe.chat/', icon: 'chat', group: 'talk' }], groups: [{ id: 'talk', name: 'Talk' }], soon: [], footer: {} };const LINKS = [{ label: 'Vibe', href: '/' }, { label: 'Global chat', href: '/global' }, { label: 'Messages', href: '/messages' }, { label: 'Message settings', href: '/messages/settings' }, { label: 'Elsewhere', href: 'https://example.test/messages/abc' }];--const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Here</title><link rel="icon" href="data:,">++const PAGE_OF = (links) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Here</title><link rel="icon" href="data:,"><script>const realFetch = window.fetch;window.fetch = (u, o) => (String(u).startsWith('https://openvibe.network/api/frame?') ? Promise.resolve({ ok: true, json: async () => (${JSON.stringify(FRAME)}) }) : String(u).startsWith('https://') ? Promise.resolve({ ok: false, status: 404, json: async () => ({}) }) : realFetch(u, o));</script><script src="/shared/navbar.js" defer></script></head><body><div id="navbar-mount"></div>--<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: null, links: ${JSON.stringify(LINKS)}, countViews: false, notifications: false }); });</script>++<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: null, links: ${JSON.stringify(links)}, countViews: false, notifications: false }); });</script></body></html>`;const server = http.createServer((req, res) => {const u = new URL(req.url, 'http://x');@@@ -34,7 +34,7 @@ const server = http.createServer((req, res) => {if (!fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(404); return res.end(); }res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, m[1])));}-- res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE);++ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE_OF(u.searchParams.has('nohome') ? LINKS.filter((l) => l.href !== '/') : LINKS));});const HERE = `[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a) => a.classList.contains('is-here') || a.getAttribute('aria-current')).map((a) => a.textContent + ':' + a.getAttribute('aria-current'))`;@@@ -69,6 +69,12 @@ const HERE = `[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a)assert.deepStrictEqual(await visit(p, '/messages/settings'), ['Message settings:page'], 'the exact page');assert.deepStrictEqual(await visit(p, '/'), ['Vibe:page'], 'the home');assert.deepStrictEqual(await visit(p, '/nowhere'), [], 'a page with no link of its own marks none');++ // A site whose links never name its home (Exchange, 11-09) gets Home first, and the home page wears it.++ assert.deepStrictEqual(await visit(p, '/?nohome=1'), ['Home:page'], 'Home is added and marked on the home');++ assert.strictEqual(await p.evaluate(`(document.querySelector('.ovs-links .ovs-link') || {}).textContent`), 'Home', 'Home leads the list');++ assert.deepStrictEqual(await visit(p, '/messages?nohome=1'), ['Messages:page'], 'elsewhere Home is not marked');++ assert.strictEqual(await p.evaluate(`[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a) => a.textContent === 'Home').length`), 1, 'one Home');++ assert.strictEqual(await (async () => { await visit(p, '/'); return p.evaluate(`[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a) => a.textContent === 'Home').length`); })(), 0, 'a site that names its home gets no second one');// On a computer the menu is a search: the field is focused.const d = await h.openPage(browser, context, { width: 1280, height: 900, block: blocked });@@@ -95,7 +95,7 @@ const server = http.createServer((req, res) => {// 4.149.0 (the owner, 11-08: "tabs at the top … the main tab should just show links that exist on that site, and// under that the full list of sites"): this site's tab, Feed and You; nothing typed opens on this site's tab.assert.strictEqual(v.tabs, 3, 'three tabs at the top');-- assert.deepStrictEqual([v.groups[0], v.links], ['On Chat', ['Rooms', 'Servers']], 'nothing typed: this site\'s own pages first');++ assert.deepStrictEqual([v.groups[0], v.links], ['On Chat', ['Home', 'Rooms', 'Servers']], 'nothing typed: this site\'s own pages first, Home leading them (no link names the home)');assert.ok(v.sites.includes('Live') && v.sites.includes('Tools'), 'then every site, even before the Frame answers');assert.strictEqual(v.placeholder, 'Search Chat, or ask Vibe', 'the site by its short name (4.48.0)');// The box completes from this site's pages (and the person's searches, history and sites); Tab takes it.
@@ -2,6 +2,10 @@
@@ -2,6 +2,10 @@
## Unreleased
## Unreleased
+- **The menu's Home.** A site whose links never name its home (Exchange: Models, Chat, Arena…) now gets Home first in
+ "On <site>", and on the home page Home wears the highlight like every other page's link (owner, 11-09). A site that
+ already names its home gets no second one.
+
- **Footer legal links are 44 px wide to a finger.** On a touch screen, Terms, Privacy, DMCA and the compact links
- **Footer legal links are 44 px wide to a finger.** On a touch screen, Terms, Privacy, DMCA and the compact links
reached 36 px across (11-09 tap audit on six sites, `tools/tap-audit.js`); their hit area now extends 5 px each side
reached 36 px across (11-09 tap audit on six sites, `tools/tap-audit.js`); their hit area now extends 5 px each side
(they sit 20 px apart, so the areas never meet).
(they sit 20 px apart, so the areas never meet).
@@ -539,7 +539,7 @@
@@ -539,7 +539,7 @@
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'b2e5ef9b3773', 'dock.js': '4dacf614797b', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075' };
+ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': '8e55bfe93238', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'ad94f524fa45', 'dock.js': '4dacf614797b', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075' };
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
@@ -965,6 +965,11 @@
@@ -965,6 +965,11 @@
// This site's own cards (a drop zone on Download, go-live status on Live, a new task on Codes…).
// This site's own cards (a drop zone on Download, go-live status on Live, a new task on Codes…).
for (const c of cards) groups.push({ title: c.title || '', as: 'card', card: c, section: `card:${c.id}`, items: [] });
for (const c of cards) groups.push({ title: c.title || '', as: 'card', card: c, section: `card:${c.id}`, items: [] });
const pages = (ctx.links || []).filter((l) => l && l.label && typeof l.href === 'string' && !l.hidden).map((l) => ({ title: l.label, url: safeUrl(l.href), icon: 'go', drawn: l.icon && typeof ctx.navIcon === 'function' ? ctx.navIcon(l.icon) : '', kind: 'page' })).filter((x) => x.url);
const pages = (ctx.links || []).filter((l) => l && l.label && typeof l.href === 'string' && !l.hidden).map((l) => ({ title: l.label, url: safeUrl(l.href), icon: 'go', drawn: l.icon && typeof ctx.navIcon === 'function' ? ctx.navIcon(l.icon) : '', kind: 'page' })).filter((x) => x.url);
+ // A site's links that never name its home get one first, so the home page has a place of its own in the menu
+ // and wears the highlight there too (owner 11-09, on openvibe.exchange: "shouldn't there be a link to home that's
+ // highlighted").
+ const rootHere = (x) => { try { const u = new URL(x.url, root.location.href); return u.origin === root.location.origin && (u.pathname.replace(/\/+$/, '') || '/') === '/'; } catch { return false; } };
+ if (pages.length && !pages.some(rootHere)) pages.unshift({ title: 'Home', url: '/', icon: 'go', drawn: typeof ctx.navIcon === 'function' ? ctx.navIcon('fa-house') : '', kind: 'page' });
// The page the person is on wears the highlight (owner 11-08): the link whose path is this page's, or the longest one
// The page the person is on wears the highlight (owner 11-08): the link whose path is this page's, or the longest one
// this page sits under (/messages/abc under Messages); "/" only on the home itself.
// this page sits under (/messages/abc under Messages); "/" only on the home itself.
let best = -1, bestLen = -1;
let best = -1, bestLen = -1;
@@ -19,13 +19,13 @@ if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
@@ -19,13 +19,13 @@ if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
const FRAME = { nav: [{ id: 'chat', name: 'Chat', url: 'https://openvibe.chat/', icon: 'chat', group: 'talk' }], groups: [{ id: 'talk', name: 'Talk' }], soon: [], footer: {} };
const FRAME = { nav: [{ id: 'chat', name: 'Chat', url: 'https://openvibe.chat/', icon: 'chat', group: 'talk' }], groups: [{ id: 'talk', name: 'Talk' }], soon: [], footer: {} };
const LINKS = [{ label: 'Vibe', href: '/' }, { label: 'Global chat', href: '/global' }, { label: 'Messages', href: '/messages' }, { label: 'Message settings', href: '/messages/settings' }, { label: 'Elsewhere', href: 'https://example.test/messages/abc' }];
const LINKS = [{ label: 'Vibe', href: '/' }, { label: 'Global chat', href: '/global' }, { label: 'Messages', href: '/messages' }, { label: 'Message settings', href: '/messages/settings' }, { label: 'Elsewhere', href: 'https://example.test/messages/abc' }];
-const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Here</title><link rel="icon" href="data:,">
+const PAGE_OF = (links) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Here</title><link rel="icon" href="data:,">
<script>
<script>
const realFetch = window.fetch;
const realFetch = window.fetch;
window.fetch = (u, o) => (String(u).startsWith('https://openvibe.network/api/frame?') ? Promise.resolve({ ok: true, json: async () => (${JSON.stringify(FRAME)}) }) : String(u).startsWith('https://') ? Promise.resolve({ ok: false, status: 404, json: async () => ({}) }) : realFetch(u, o));
window.fetch = (u, o) => (String(u).startsWith('https://openvibe.network/api/frame?') ? Promise.resolve({ ok: true, json: async () => (${JSON.stringify(FRAME)}) }) : String(u).startsWith('https://') ? Promise.resolve({ ok: false, status: 404, json: async () => ({}) }) : realFetch(u, o));
</script>
</script>
<script src="/shared/navbar.js" defer></script></head><body><div id="navbar-mount"></div>
<script src="/shared/navbar.js" defer></script></head><body><div id="navbar-mount"></div>
-<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: null, links: ${JSON.stringify(LINKS)}, countViews: false, notifications: false }); });</script>
+<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: null, links: ${JSON.stringify(links)}, countViews: false, notifications: false }); });</script>
</body></html>`;
</body></html>`;
const server = http.createServer((req, res) => {
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
const u = new URL(req.url, 'http://x');
@@ -34,7 +34,7 @@ const server = http.createServer((req, res) => {
@@ -34,7 +34,7 @@ const server = http.createServer((req, res) => {
if (!fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(404); return res.end(); }
if (!fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(404); return res.end(); }
res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, m[1])));
res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, m[1])));
}
}
- res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE);
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGE_OF(u.searchParams.has('nohome') ? LINKS.filter((l) => l.href !== '/') : LINKS));
});
});
const HERE = `[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a) => a.classList.contains('is-here') || a.getAttribute('aria-current')).map((a) => a.textContent + ':' + a.getAttribute('aria-current'))`;
const HERE = `[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a) => a.classList.contains('is-here') || a.getAttribute('aria-current')).map((a) => a.textContent + ':' + a.getAttribute('aria-current'))`;
@@ -69,6 +69,12 @@ const HERE = `[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a)
@@ -69,6 +69,12 @@ const HERE = `[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a)
assert.deepStrictEqual(await visit(p, '/messages/settings'), ['Message settings:page'], 'the exact page');
assert.deepStrictEqual(await visit(p, '/messages/settings'), ['Message settings:page'], 'the exact page');
assert.deepStrictEqual(await visit(p, '/'), ['Vibe:page'], 'the home');
assert.deepStrictEqual(await visit(p, '/'), ['Vibe:page'], 'the home');
assert.deepStrictEqual(await visit(p, '/nowhere'), [], 'a page with no link of its own marks none');
assert.deepStrictEqual(await visit(p, '/nowhere'), [], 'a page with no link of its own marks none');
+ // A site whose links never name its home (Exchange, 11-09) gets Home first, and the home page wears it.
+ assert.deepStrictEqual(await visit(p, '/?nohome=1'), ['Home:page'], 'Home is added and marked on the home');
+ assert.strictEqual(await p.evaluate(`(document.querySelector('.ovs-links .ovs-link') || {}).textContent`), 'Home', 'Home leads the list');
+ assert.deepStrictEqual(await visit(p, '/messages?nohome=1'), ['Messages:page'], 'elsewhere Home is not marked');
+ assert.strictEqual(await p.evaluate(`[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a) => a.textContent === 'Home').length`), 1, 'one Home');
+ assert.strictEqual(await (async () => { await visit(p, '/'); return p.evaluate(`[...document.querySelectorAll('.ovs-links .ovs-link')].filter((a) => a.textContent === 'Home').length`); })(), 0, 'a site that names its home gets no second one');
// On a computer the menu is a search: the field is focused.
// On a computer the menu is a search: the field is focused.
const d = await h.openPage(browser, context, { width: 1280, height: 900, block: blocked });
const d = await h.openPage(browser, context, { width: 1280, height: 900, block: blocked });
@@ -95,7 +95,7 @@ const server = http.createServer((req, res) => {
@@ -95,7 +95,7 @@ const server = http.createServer((req, res) => {
// 4.149.0 (the owner, 11-08: "tabs at the top … the main tab should just show links that exist on that site, and
// 4.149.0 (the owner, 11-08: "tabs at the top … the main tab should just show links that exist on that site, and
// under that the full list of sites"): this site's tab, Feed and You; nothing typed opens on this site's tab.
// under that the full list of sites"): this site's tab, Feed and You; nothing typed opens on this site's tab.
assert.strictEqual(v.tabs, 3, 'three tabs at the top');
assert.strictEqual(v.tabs, 3, 'three tabs at the top');
- assert.deepStrictEqual([v.groups[0], v.links], ['On Chat', ['Rooms', 'Servers']], 'nothing typed: this site\'s own pages first');
+ assert.deepStrictEqual([v.groups[0], v.links], ['On Chat', ['Home', 'Rooms', 'Servers']], 'nothing typed: this site\'s own pages first, Home leading them (no link names the home)');
assert.ok(v.sites.includes('Live') && v.sites.includes('Tools'), 'then every site, even before the Frame answers');
assert.ok(v.sites.includes('Live') && v.sites.includes('Tools'), 'then every site, even before the Frame answers');
assert.strictEqual(v.placeholder, 'Search Chat, or ask Vibe', 'the site by its short name (4.48.0)');
assert.strictEqual(v.placeholder, 'Search Chat, or ask Vibe', 'the site by its short name (4.48.0)');
// The box completes from this site's pages (and the person's searches, history and sites); Tab takes it.
// The box completes from this site's pages (and the person's searches, history and sites); Tab takes it.