openvibers/openvibe.watch Commits

The home on phones: the ready-made watches are chips, the questions a fold (#142)

OP OpenVibers committed 14 days ago · 62509d1ffd · 5 files, +214 −6

The home page is one or two screens on a phone. The ready-made watches (hosting
prices, feeds, blogs) become one pick block: a row of chips picks the group a
phone shows, and every group stays in the page for crawlers. The questions
become a fold, closed on a phone and open where there is room. A wider screen
shows every group and no chips, as before.

Tested with the site's suite (home-short.test.js covers the chips, the CSS
guard and the fold).

Changes

ddiff --git a/public/css/home.css b/public/css/home.css
iindex aab8f3a..a62e4f2 100644
---- a/public/css/home.css
++++ b/public/css/home.css
@@@ -61,3 +61,50 @@
.hm-grid > li { flex: 0 0 70%; scroll-snap-align: start; }
.hm-fold > summary span { display: none; }
}
++
++/* The phone home (2026-10-30, the owner's short-pages rule): the ready-made watches are chips, one group at a time,
++ and the questions are a fold. Both only work under 640 px, so a wider screen shows every group and no chips. */
++/* Pick (the owner's short-pages rule, 2026-10-30): on a phone the front page's blocks of the same kind are one at a
++ time, picked with a row of chips. Radio inputs and :has(), no script: the chips and the one-pane rule live under
++ 640 px and only where :has() is understood, so a wider screen, or an older browser, shows every pane and no chips.
++ Every pane stays in the page for crawlers. */
++.pick-head { display: none; }
++.pick-row { display: none; }
++@supports selector(:has(*)) { @media (max-width: 640px) {
++ .pick-bar { margin-top: 30px; }
++ .pick-head { display: block; }
++ .pick-head h2 { margin: 0 0 4px; font: 700 clamp(20px, 2.4vw, 28px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); letter-spacing: -.02em; }
++ .pick-row { display: flex; gap: 8px; overflow-x: auto; margin: 10px -16px 0; padding: 2px 16px 4px; scrollbar-width: none; overscroll-behavior-x: contain; }
++ .pick-row::-webkit-scrollbar { display: none; }
++ .pick-row input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
++ .pick-row label { display: inline-flex; align-items: center; flex: none; min-height: 44px; padding: 0 15px; border-radius: 999px; border: 1px solid var(--border, #1f2d47); color: var(--text-secondary, #96a7c2); font-size: .9rem; font-weight: 650; white-space: nowrap; cursor: pointer; }
++ .pick-row input:checked + label { color: var(--text-primary, #e6edf7); border-color: var(--accent, #3b82f6); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }
++ .pick-row input:focus-visible + label { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
++ .pick .pick-pane { display: none; }
++ /* A link that points at a pane opens that pane on a phone too. */
++ .pick .pick-pane:target { display: block !important; }
++ .pick:has(.pick-pane:target) .pick-pane:not(:target) { display: none !important; }
++ /* The chip names the pane, so the pane's own heading gives way to it; the link to more stays. */
++ .pick .pick-pane > h2, .pick .pick-pane > h3 { display: none; }
++} }
++
++/* The fold (the owner's short-pages rule): closed on a phone, and where there is room it looks like the section it
++ replaces: a heading and its content, with no card around it. */
++.hm-fold-block { margin-top: 32px; }
++.hm-fold-block > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 44px; cursor: pointer; list-style: none; }
++.hm-fold-block > summary::-webkit-details-marker { display: none; }
++.hm-fold-block > summary h2 { margin: 0; font: 700 clamp(19px, 2.2vw, 26px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); }
++.hm-fold-block > summary span { color: var(--text-muted, #7386a3); font-weight: 600; font-size: .86rem; white-space: nowrap; }
++.hm-fold-block > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
++@media (min-width: 641px) {
++ .hm-fold-block { border: 0; background: none; border-radius: 0; margin: 56px auto 0; padding: 0; }
++ .hm-fold-block > summary { display: block; min-height: 0; }
++ .hm-fold-block > summary h2 { margin: 0 0 6px; font: 700 clamp(22px, 2.6vw, 30px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); }
++ .hm-fold-block > summary span { display: none; }
++ .hm-fold-block .hm-sec, .hm-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }
++}
++@media (max-width: 640px) { .hm-fold-block { margin-top: 24px; } .hm-fold-block > summary h2 { font-size: 1.05rem; } }
++/* A fold that sits inside the section it replaces adds no space of its own. */
++:where(.sc-sec, .cm-sec, .hm-sec, .section) > .hm-fold-block { margin: 0; }
++/* The fold holds the questions, so its list gets the room the card gives it. */
++.hm-fold-block .hm-faq { margin: 0; padding: 0 16px 16px; }
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 86e3dba..ecc8186 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -24,6 +24,7 @@ const { send } = require('../render/layout');
const W = require('../render/watch');
const changelog = require('../render/changelog');
const { createDiscoveryRoutes, homeJsonLd, DESCRIPTION } = require('./discovery');
++const S = require('../render/short');
const { createNetworkReads } = require('./network-reads');
const seo = require('openvibe-shared/seo');
@@@ -567,11 +568,14 @@ ${field('The coding task', html`<textarea id="${idOf('codes_brief')}" name="code
const blogs = [...postRows.reduce((m, p) => (m.has(p.blog) ? m : m.set(p.blog, p)), new Map()).values()].slice(0, 3);
const card = ({ title, detail, href, label, extra = '' }) => html`<li class="hm-card"><div class="hm-card-body"><b>${title}</b>${detail ? html`<span>${detail}</span>` : ''}${raw(extra)}</div><a class="hm-go" href="${href}">${label}</a></li>`;
const readyGroups = [
-- plans.length ? { title: 'Hosting prices', lede: 'Plans on OpenVibe.Host, read from its catalog API.', items: plans.map((p) => card({ title: p.title, detail: p.detail, href: newWatch({ template: 'json', url: `${p.origin}/api/v1/catalog/plans/${p.id}`, value_path: 'plan.price_monthly', op: 'changed', name: `Price: ${p.title}`.slice(0, 80) }), label: 'Tell me when the price changes' })) } : null,
-- { title: 'Feeds', lede: 'Told the moment a new item appears.', items: FEEDS.map((f) => card({ title: f.title, detail: f.detail, href: newWatch({ template: 'feed', url: f.url, name: f.title.slice(0, 80) }), label: 'Watch this feed' })) },
-- blogs.length ? { title: 'Blogs on OpenVibe', lede: 'Woken by the blog.post.published event, so nothing is polled.', items: blogs.map((b) => card({ title: `New posts on @${b.blog}`, detail: `Latest: ${b.title}`, href: newWatch({ template: 'event', event: 'blog.post.published', event_filter: b.blog, name: `New posts on @${b.blog}` }), label: 'Tell me about new posts' })) } : null,
++ plans.length ? { id: 'prices', title: 'Hosting prices', lede: 'Plans on OpenVibe.Host, read from its catalog API.', items: plans.map((p) => card({ title: p.title, detail: p.detail, href: newWatch({ template: 'json', url: `${p.origin}/api/v1/catalog/plans/${p.id}`, value_path: 'plan.price_monthly', op: 'changed', name: `Price: ${p.title}`.slice(0, 80) }), label: 'Tell me when the price changes' })) } : null,
++ { id: 'feeds', title: 'Feeds', lede: 'Told the moment a new item appears.', items: FEEDS.map((f) => card({ title: f.title, detail: f.detail, href: newWatch({ template: 'feed', url: f.url, name: f.title.slice(0, 80) }), label: 'Watch this feed' })) },
++ blogs.length ? { id: 'blogs', title: 'Blogs on OpenVibe', lede: 'Woken by the blog.post.published event, so nothing is polled.', items: blogs.map((b) => card({ title: `New posts on @${b.blog}`, detail: `Latest: ${b.title}`, href: newWatch({ template: 'event', event: 'blog.post.published', event_filter: b.blog, name: `New posts on @${b.blog}` }), label: 'Tell me about new posts' })) } : null,
].filter(Boolean);
const faqLd = seo.jsonLd.faq(FAQ.map(([q, a]) => ({ q, a })));
++ // The home on a phone (the owner's short-pages rule, 2026-10-30): the ready-made watches are chips, one group
++ // at a time, and every group stays in the page for crawlers.
++ const readyPanes = readyGroups.map((g) => ({ id: g.id, label: g.title, html: html`<h3 class="hm-group">${g.title}</h3><p class="hm-group-lede">${g.lede}</p><ul class="hm-grid">${g.items}</ul>`.s }));
page(req, res, {
index: true,
cache: signedIn(req) ? null : cache.htmlHeaders({ maxAge: 300 }),
@@@ -579,6 +583,8 @@ ${field('The coding task', html`<textarea id="${idOf('codes_brief')}" name="code
description: DESCRIPTION,
styles: [showcase.STYLESHEET],
css: ['css/home.css'],
++ // The rule that shows the picked group: one per group, so the CSS is written for this page.
++ inlineStyles: [`@supports selector(:has(*)){@media (max-width:640px){${S.pickRules('ready', readyPanes.map((p) => p.id))}}}`],
body: html`${raw(hero)}
${raw(showcase.features({
id: 'start',
@@@ -591,7 +597,7 @@ ${channels.length ? html`<section class="sc-sec" aria-labelledby="h-live"><div c
<ul class="hm-grid">${channels.map((c) => html`<li class="hm-card"><div class="hm-card-body"><b><a href="${c.url}">@${c.username}</a></b><span>${[c.category, c.followers ? `${c.followers} follower${c.followers === 1 ? '' : 's'}` : ''].filter(Boolean).join(' · ')}</span>${c.live ? raw('<span class="hm-live">Live now</span>') : ''}</div><a class="hm-go" href="${newWatch({ template: 'event', event: 'live.stream.started', event_filter: c.username, name: `@${c.username} goes live` })}">Tell me when they go live</a></li>`)}</ul></section>` : ''}
<section class="sc-sec" aria-labelledby="h-ready"><h2 id="h-ready">Ready-made watches</h2>
<p class="sc-lede">Each opens the form already filled in. Check it, change anything, and save.</p>
--${readyGroups.map((g) => html`<h3 class="hm-group">${g.title}</h3><p class="hm-group-lede">${g.lede}</p><ul class="hm-grid">${g.items}</ul>`)}</section>
++${raw(S.pickBlock({ key: 'ready', label: 'Ready-made watches', panes: readyPanes }))}</section>
<section class="sc-sec" aria-labelledby="h-check"><details class="hm-fold"><summary><h2 id="h-check">How it checks</h2><span>from a URL to a notification, politely</span></summary>${raw(showcase.steps({
id: 'steps',
title: 'From a URL to a notification',
@@@ -617,8 +623,7 @@ ${readyGroups.map((g) => html`<h3 class="hm-group">${g.title}</h3><p class="hm-g
{ label: 'Read what it found (curl)', lang: 'sh', code: `# Your watches\ncurl -H "Authorization: Bearer $OPENVIBE_TOKEN" \\\n ${config.baseUrl}/api/v1/watches\n\n# Every value one watch observed, newest first\ncurl -H "Authorization: Bearer $OPENVIBE_TOKEN" \\\n ${config.baseUrl}/api/v1/watches/wch_…/observations` },
],
}))}</details></section>
--<section class="sc-sec" id="faq" aria-labelledby="h-faq"><h2 id="h-faq">Questions</h2>
--<div class="hm-faq">${FAQ.map(([q, a], i) => html`<details class="hm-qa"${i === 0 ? raw(' open') : ''}><summary><h3>${q}</h3></summary><p>${a}</p></details>`)}</div></section>
++<section class="sc-sec" id="faq" aria-labelledby="h-faq">${raw(S.foldBlock({ id: 'faq-fold', head: '<h2 id="h-faq">Questions</h2>', html: html`<div class="hm-faq">${FAQ.map(([q, a], i) => html`<details class="hm-qa"${i === 0 ? raw(' open') : ''}><summary><h3>${q}</h3></summary><p>${a}</p></details>`)}</div>`.s }))}</section>
${summary ? html`<section class="sc-sec" aria-labelledby="h-yours"><h2 id="h-yours">Your watches</h2>
<p>${summary.total ? html`You have <strong>${summary.total}</strong> watch${summary.total === 1 ? '' : 'es'}: ${summary.active} active, ${summary.paused} paused, ${summary.disabled} disabled, ${summary.failed} failing.` : 'You have no watches yet.'} <a href="/watches">See them</a> or <a href="/watches/new?template=page">start one</a>.</p></section>` : ''}`,
});
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex b2a5c03..a7a88eb 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -91,6 +91,7 @@ ${appIcon.headTags({ site: 'watch' })}
${(o.styles || []).map((name) => `<link rel="stylesheet" href="${esc(ovServe.url(name))}">`).join('\n')}
<link rel="stylesheet" href="${asset('css/app.css')}">
${(o.css || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">`).join('\n')}
++${(o.inlineStyles || []).map((css) => `<style>${css}</style>`).join('\n')}
${render(scripts)}
<meta name="ov-boost" content="watch@${RELEASE}">
<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
ddiff --git a/server/render/short.js b/server/render/short.js
nnew file mode 100644
iindex 0000000..d201b02
---- /dev/null
++++ b/server/render/short.js
@@@ -0,0 +1,86 @@
++'use strict';
++/**
++ * The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
++ *
++ * pickBlock() several blocks of the same kind, one at a time on a phone, picked with a row of chips. Radio
++ * inputs and :has(), no script: every block stays in the page for crawlers, and a wider screen (or a
++ * browser without :has()) shows them all and no chips.
++ * foldBlock() a part a reader rarely needs, closed on a phone and open where there is room. The page's own markup
++ * keeps it open, so a browser without the script shows it as before; the script closes it on a narrow
++ * screen and remembers the reader's choice.
++ *
++ * The CSS for both lives in the site's own stylesheet; the rule that shows the picked block is written for the
++ * page, because there is one per block.
++ */
++const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
++
++/**
++ * A pick block. The panes are the page's own blocks; each one keeps its heading on a wide screen, and on a phone the
++ * heading gives way to the chip that names it.
++ *
++ * pickBlock({ key, label, head, panes }) head: the heading of the block, or '' when the block already has one.
++ * panes: [{ id, label, html }]
++ */
++function pickBlock({ key, label, head = '', panes, bodyClass = '' }) {
++ // A pane with nothing to show is left out, so the chips and the rules that show them match the page.
++ panes = (panes || []).filter((p) => p.html);
++ if (!panes.length) return '';
++ const chips = panes.map((p, i) => `<input type="radio" name="pick-${esc(key)}" id="pk-${esc(key)}-${esc(p.id)}" value="${esc(p.id)}"${i === 0 ? ' checked' : ''}><label for="pk-${esc(key)}-${esc(p.id)}">${esc(p.label)}</label>`).join('');
++ // The pane carries the id the page already links to (an anchor in the hero, for instance), so a link to it still
++ // opens that pane on a phone (the CSS shows the pane a link points at).
++ const body = panes.map((p) => `<div class="pick-pane" id="${esc(p.id)}">${p.html}</div>`).join('');
++ const bar = `${head ? `<div class="pick-head"><h2>${esc(head)}</h2></div>` : ''}<div class="pick-row" role="radiogroup" aria-label="${esc(label)}">${chips}</div>`;
++ return `<div class="pick" id="pick-${esc(key)}"><div class="pick-bar">${bar}</div><div class="pick-body${bodyClass ? ' ' + esc(bodyClass) : ''}">${body}</div></div>`;
++}
++
++/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */
++function pickRule(key, id) {
++ return `.pick:has(#pk-${key}-${id}:checked) #${id}{display:block}`;
++}
++
++/** The rules for every block of a pick block, in the order they appear. */
++function pickRules(key, ids) {
++ return ids.map((id) => pickRule(key, id)).join('');
++}
++
++/**
++ * A fold. The head is the heading the block had; the html is what it holds.
++ *
++ * foldBlock({ id, head, html })
++ */
++function foldBlock({ id, head, html }) {
++ return `<details class="hm-fold hm-fold-block" id="${esc(id)}" open><summary>${head}<span>tap to open</span></summary>${html}</details>${foldScript(id)}`;
++}
++
++/** The script a fold carries: closed on a phone unless the reader opened it before, and it remembers that. */
++function foldScript(id) {
++ return `<script>(function(){var d=document.getElementById(${JSON.stringify(id)});if(!d)return;var k='ov_fold_'+${JSON.stringify(id)};var s=null;try{s=localStorage.getItem(k)}catch(e){}if(s==='open'){d.open=true;return}if(s==='closed'){d.open=false;return}var t=0;if(window.matchMedia&&window.matchMedia('(max-width:640px)').matches){t=Date.now();d.open=false}d.addEventListener('toggle',function(){if(Date.now()-t<300)return;try{localStorage.setItem(k,d.open?'open':'closed')}catch(e){}})})();</script>`;
++}
++
++/**
++ * A section of the page's own markup as a fold. The section stays where it was; its own heading becomes the fold's
++ * heading, and the fold is closed on a phone (the script) and open where there is room.
++ *
++ * foldSection(html, needle, id) needle: a mark of the part to fold (a class, for instance)
++ */
++function foldSection(html, needle, id) {
++ const at = html.indexOf(needle);
++ if (at < 0) return html;
++ const start = html.lastIndexOf('<section', at);
++ const close = html.indexOf('</section>', at);
++ if (start < 0 || close < 0) return html;
++ const openEnd = html.indexOf('>', start) + 1;
++ const openTag = html.slice(start, openEnd);
++ let inner = html.slice(openEnd, close);
++ const h2 = /<h2[^>]*>[\s\S]*?<\/h2>/.exec(inner);
++ if (!h2) return html;
++ const head = h2[0];
++ inner = inner.slice(h2.index + head.length);
++ // The lede under the heading goes with it: the fold's heading says enough on a phone.
++ const lede = /<p class="sc-lede"[^>]*>[\s\S]*?<\/p>/.exec(inner);
++ if (lede) inner = inner.slice(0, lede.index) + inner.slice(lede.index + lede[0].length);
++ const fold = `<details class="hm-fold hm-fold-block" id="${esc(id)}" open><summary>${head}<span>tap to open</span></summary>${inner.trim()}</details>${foldScript(id)}`;
++ return html.slice(0, start) + openTag + fold + html.slice(close);
++}
++
++module.exports = { pickBlock, pickRule, pickRules, foldBlock, foldScript, foldSection };
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
nnew file mode 100644
iindex 0000000..393cb90
---- /dev/null
++++ b/test/home-short.test.js
@@@ -0,0 +1,69 @@
++'use strict';
++/**
++ * The home on a phone (the owner's short-pages rule, 2026-10-30): the ready-made watches are a pick block, a row of
++ * chips picks one of the groups, and the questions are a fold. Radio inputs and :has(), no script for the chips;
++ * every group stays in the page for crawlers; a wider screen shows them all and no chips.
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const { boot, site, suite } = require('./helpers');
++
++const t = suite('home-short');
++let svc;
++let search;
++
++const doc = (o) => ({ visibility: 'public', title: 'x', summary: '', facets: {}, ...o });
++const RESULTS = {
++ 'live/channel': [],
++ 'host/plan': [doc({ owner: 'host', type: 'plan', id: 'hpl_01M4Z4ZZ6B8FC1ZEV0KAQ9XFBP', title: 'A small plan', summary: '2 vCPU', canonical_url: 'https://openvibe.host/catalog/plans/hpl_01M4Z4ZZ6B8FC1ZEV0KAQ9XFBP' })],
++ 'blog/post': [doc({ owner: 'blog', type: 'post', id: 'a', title: 'Notes', canonical_url: 'https://openvibe.blog/@openvibe/a', facets: { blog: 'openvibe' } })],
++};
++
++t('boot', async () => {
++ search = await site({ '/api/v1/search': (req, res, { url }) => ({ headers: { 'content-type': 'application/json' }, body: JSON.stringify({ results: RESULTS[`${url.searchParams.get('owner')}/${url.searchParams.get('type')}`] || [] }) }) });
++ svc = await boot({ env: { WATCH_SEARCH_URL: search.origin } });
++});
++
++t('the ready-made watches are chips: one per group, the first picked, every group in the page', async () => {
++ const home = await svc.get('/', { headers: { accept: 'text/html' } });
++ assert.strictEqual(home.status, 200, home.text);
++ const page = home.text;
++ assert.ok(page.includes('<div class="pick" id="pick-ready">'), 'the ready-made watches are a pick block');
++ const block = page.slice(page.indexOf('<div class="pick" id="pick-ready">'));
++ const chips = [...block.matchAll(/<input type="radio" name="pick-ready" id="pk-ready-([a-z]+)" value="\1"( checked)?><label for="pk-ready-\1">([^<]+)<\/label>/g)];
++ assert.deepStrictEqual(chips.map((m) => m[1]), ['prices', 'feeds', 'blogs'], 'a chip per group');
++ assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), ['prices'], 'the first group is picked');
++ // Every group stays in the page, with the id its chip picks.
++ const panes = [...block.matchAll(/<div class="pick-pane" id="([a-z]+)">/g)].map((m) => m[1]);
++ assert.deepStrictEqual(panes, ['prices', 'feeds', 'blogs'], 'every group is in the page, one per chip');
++ // Nothing was removed: the groups and their links are still there.
++ for (const title of ['Hosting prices', 'Feeds', 'Blogs on OpenVibe', 'Watch this feed', 'Ready-made watches']) {
++ assert.ok(page.includes(title), `${title} is on the page`);
++ }
++});
++
++t('the CSS guard: the chips are hidden and every group shows until 640 px and where :has() is understood', async () => {
++ const home = await svc.get('/', { headers: { accept: 'text/html' } });
++ const page = home.text;
++ assert.match(page, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick:has\(#pk-ready-prices:checked\) #prices\{display:block\}/, 'the rule that shows the picked group');
++ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'home.css'), 'utf8');
++ assert.match(css, /\.pick-row \{ display: none; \}/, 'the chips are hidden by default');
++ assert.match(css, /@supports selector\(:has\(\*\)\) \{ @media \(max-width: 640px\) \{/, 'the phone rules are under :has() and 640 px');
++ assert.match(css, /\.pick-row label \{[^}]*min-height: 44px/, 'a chip is a 44 px target');
++ assert.match(css, /\.pick \.pick-pane \{ display: none; \}/, 'the groups are hidden on a phone');
++});
++
++t('the questions are a fold, open in the page and closed on a phone', async () => {
++ const home = await svc.get('/', { headers: { accept: 'text/html' } });
++ const page = home.text;
++ assert.ok(page.includes('<details class="hm-fold hm-fold-block" id="faq-fold" open>'), 'the questions are a fold, open in the page');
++ assert.ok(page.includes('document.getElementById("faq-fold")'), 'the fold has the script that closes it on a phone');
++ // The questions stay in the page.
++ assert.match(page, /What can I watch\?/);
++ assert.ok(page.includes('<section class="sc-sec" id="faq" aria-labelledby="h-faq">'), 'the questions keep their section');
++});
++
++t('done', async () => { await svc.stop(); await search.close(); });
++
++t.run();