openvibers/openvibe.design Commits
The wait is the mark's own, Thinking has its page, and nine rules from 2026-11-10 (#240)
Changes
@@@ -43,6 +43,42 @@ const RULES = Object.freeze(['Motion has four settings: Automatic (follows the device), Full, Calm and Off. Under Calm, and on a device that asks for less motion, every effect still plays: a third of the distance, no overshoot, endless loops at 0.6 of their speed. A page never stops an effect because motion is reduced; a rule that stops one belongs to Off (html[data-ov-motion=off]). A distance of 24px or more is multiplied by --ov-move. Only what starts by itself and fills the view (a video, a carousel) still waits for a tap under Calm.','2026-11-10', 'Reduced motion gave every animation no length at all, so Calm pages looked broken: the owner asked for motion reduced, not removed.',[{ label: 'Motion', href: '/motion' }, { label: 'Your look', href: '/look' }], ['openvibe-shared test: motion-setting']),++ R('effects-by-device-not-screen', 'motion', 'An effect is left out by what the device says, never by the size of its screen.',++ 'A site may leave out an effect a device cannot afford, by what the device says about itself: 2 GB of memory or less (navigator.deviceMemory), or a person saving data (navigator.connection.saveData). It never does so because the screen is narrow or the pointer is coarse, and never by overriding the person’s Motion setting. What follows a pointer (a spotlight, a tilt) may be left out where there is no pointer; a layout rule under a width (a collage that has no room) is not a motion rule.',++ '2026-11-10', 'OpenVibe.Live marked every phone "lite" and switched its home’s animations off whatever the Motion setting said: on a phone nothing moved, and the owner asked why with motion on.',++ [{ label: 'Motion', href: '/motion' }], ['OpenVibe.Live test: home-lite']),++ R('a-dock-travels', 'motion', 'A control that docks moves to its place.',++ 'An action that pins itself to the screen’s edge while its real control is out of view rises into place, and when the real control comes back into view the pinned one travels to it and hands over. It never appears and vanishes in one frame. Once the page’s footer is on screen it rides above the footer instead of lying over it. With Motion off it is simply there or not.',++ '2026-11-10', 'The Go live bar on a phone flashed in and out as the page scrolled, and sat over the footer at the page’s end.',++ [{ label: 'Motion', href: '/motion' }], ['OpenVibe.Live test: golive-setup']),++ R('a-fold-shows-its-first', 'length', 'A fold shows its first two and fades.',++ 'A list shortened on a phone shows its first two items whole and fades into the third, with one button under it that says how many there are, opens the rest and closes them again. A closed fold that shows only a heading and a count does not read as something to open, and what is in it cannot be seen. Every item stays in the page.',++ '2026-11-10', 'Community’s Questions was a heading and "6 questions": the owner did not see that it opened, or a single question.',++ [{ label: 'Patterns', href: '/patterns' }], ['OpenVibe.Community test: home-short']),++ R('the-rail-names-its-places', 'navigation', 'A place is named, not initialled.',++ 'A strip or rail of places a person switches between carries each place’s name: beside its mark where there is room (a phone’s strip is named chips), and as a label that follows the pointer where the rail is a column of tiles. Two initials in a circle are a mark, not a name. A place that can be managed opens its sheet when held, right-clicked, or tapped again while open: what it is, the person’s rank in it, and what that rank may do.',++ '2026-11-10', 'Chat’s server strip was circles reading "O" and "T": the owner did not know what they were or how to remove one.',++ [{ label: 'Patterns', href: '/patterns' }], ['OpenVibe.Chat test: shell']),++ R('one-place-once', 'navigation', 'A destination is offered once on a screen.',++ 'Where a phone has the bottom bar, a strip above the page does not repeat the bar’s places, and one icon means one thing on a site: the globe is not both "Servers" and "Global chat". A link in a bar goes to a page that exists.',++ '2026-11-10', 'Chat showed Vibe at the top and at the bottom, used the globe for two places, and its Activity button opened a page that had never existed.',++ [{ label: 'The Frame', href: '/system' }], ['OpenVibe.Chat test: shell']),++ R('a-hero-does-not-name-the-site', 'words', 'A hero does not say the site’s name above its title.',++ 'The bar already says which site this is. A line above a hero’s title is there only when it says something the bar does not: a state ("Live now"), a count, a date. A hero shows what the product does, with something real from it, before it describes it.',++ '2026-11-10', 'openvibe.exchange opened with "OPENVIBE.EXCHANGE" over its title, under a bar that reads Exchange.',++ [{ label: 'Patterns', href: '/patterns' }]),++ R('a-handle-is-written-with-its-at', 'words', 'A link to an @address says @name.',++ 'Where a row, a card or a link opens an /@name address and names it by the handle, the handle is written @name: the words are the address. A display name stays a display name, with the @handle on the second line where the row has one. Not in a chat line’s author and not in running text a person wrote.',++ '2026-11-10', 'A profile listed "Maticus · Live channel" for a card that opens openvibe.live/@Maticus.',++ [{ label: 'Patterns', href: '/patterns' }], ['OpenVibe.Network test: public-profile']),++ R('a-computer-is-added-once', 'products', 'A computer is added in one place, and picked everywhere.',++ 'A person adds a computer at openvibe.link. Every other site picks from the person’s Link machines with the one shared picker and never asks anyone to type a machine’s id. A site may show which machine it uses and what it is doing there; it keeps no list of its own, no "add a machine" of its own and no machines tab.',++ '2026-11-10', 'Actor asked for a "Link machine ID" in a text field, beside other sites with machine pages of their own.',++ [{ label: 'The system', href: '/system' }]),++ R('one-composer', 'look', 'One composer, wherever a person writes.',++ 'Every place a person writes to people or to an assistant uses the shared composer: one box with the text in it, Add on its left where the place takes files, what the place offers (tools, a model) in a row inside it, Send on its right, lit while there is something to send. What is being added waits in a tray above the text with its progress. A feature added to the composer arrives on every site; a site does not grow a composer of its own.',++ '2026-11-10', 'Chat had four hand-written composers and a picture button outside the box; Actor and Vibe had two more shapes of the same thing.',++ [{ label: 'Patterns', href: '/patterns' }], ['OpenVibe.Chat test: shell']),R('count-what-it-says', 'honest', 'A number counts exactly what its label says.','A count next to a list equals the length of that list. "4 agents working now" must count agents at work, not the agent catalogue.','2026-11-01', 'A home page counted its catalogue as "working now".',@@@ -92,9 +128,9 @@ const RULES = Object.freeze(['A setting has a single control, and an action a single button on the page. When a picture of the choices can be pressed, the pictures are the control: no second switch beside them saying the same thing.','2026-11-09', 'Your look had the icon style as a row of three words and again as three screens; Themes asked people to sign in twice on one screen.',[{ label: 'Your look', href: '/look' }]),-- R('no-spinners', 'motion', 'No spinning rings. Things move only while something is actually happening.',-- 'Loading shows the shape of what is coming (a shimmer placeholder of the right size) or the mark at work. A finished or idle thing is still.',-- '2026-10-31', 'Spinning rings around icons everywhere looked cheap and said nothing.',++ R('no-spinners', 'motion', 'The only thing that goes round is the mark\u2019s own O.',++ 'A wait shows the shape of what is coming (a placeholder of the right size) or the mark at work: its O breaks into dots that go round, and three dots rise inside it while an agent thinks or writes. No borrowed spinner, and no ring chasing its tail round an icon. A finished or idle thing is still.',++ '2026-11-10', 'Spinning rings round icons everywhere looked cheap and said nothing (2026-10-31). Then the mark only breathing did not read as loading, so its own O became the one thing that goes round.',[{ label: 'Loading', href: '/loading' }, { label: 'The mark at work', href: '/patterns/mark-system' }], ['test/no-spin.test.js']),R('streams-hold-still', 'motion', 'A live list changes only where it changed.','Patch rows in place, keep the reader’s place, follow the bottom only when they were there, and fade in only what is new.',@@@ -63,7 +63,8 @@ const live = (attrs = '', o = {}) => `<span class="ov-mark" ${attrs}>${art(o)}</const STATES = [['', 'At rest', 'Still. It moves for a moment on your first visit, and when you point at it.'],-- ['busy', 'Working', 'The V bobs while the O breathes, in turn: something is on its way.'],++ ['busy', 'Working', 'The O breaks into dots that go round while the V bobs: something is on its way.'],++ ['think', 'Thinking', 'Three dots rise inside the O and the V glimmers: your agent is thinking or writing.'],['ok', 'Done', 'The V becomes a tick and goes green. The tick only ever means done.'],['error', 'Failed', 'The V becomes an exclamation mark, red, and the mark shakes once.'],['no', 'Declined', 'The V becomes a cross, grey: cancelled, turned down, not now.'],@@@ -88,7 +89,7 @@ function brandPage({ base = 'https://openvibe.design' } = {}) {</header>`;const moves = `<section class="br-sec" aria-labelledby="br-moves">-- <div class="br-sec-head"><h2 id="br-moves">It moves when it means something</h2><p>Every move is a word: one shape says working, done, failed and live, in colour and in form. Nothing spins.</p></div>++ <div class="br-sec-head"><h2 id="br-moves">It moves when it means something</h2><p>Every move is a word: one shape says working, thinking, done, failed and live, in colour and in form. The only thing that goes round is its own O.</p></div><div class="br-play"><div class="br-play-stage">${live('data-size="190" data-wide data-br-demo', { wide: true })}</div><div class="br-play-side">@@@ -49,7 +49,7 @@ function loadingPage({ base = 'https://openvibe.design' } = {}) {['A list shows its shape at once.', 'Its placeholders are in the first HTML, the same size as what replaces them, so filling in moves nothing (each demo above measures it).'],['A picture sits on its own colour first.', 'Its dominant colour, or a tiny blurred copy, until it arrives. The first picture asks for itself first (fetchpriority="high"); the rest wait until they are near (loading="lazy").'],['Moving between pages feels like one app.', 'The mark works in the navbar with a two-pixel line under it. Links are fetched when pointed at (speculation rules), and the navbar and app bar stay still while the page cross-fades (view transitions).'],-- ['Nothing spins.', 'The mark bobs and breathes, a line fills. Calm and reduced motion keep everything still.'],++ ['The wait is the mark\u2019s own.', 'Its O goes round as dots while it works, three dots rise inside it while an agent thinks, and a line fills when the length is known. Calm plays it slower; only motion off keeps it still.'],['Slow says so.', 'After a few seconds a load explains itself and offers what to do. A failure says what happened and offers Retry, never an endless wait.'],['Off screen costs nothing.', 'Long lists skip the work for what is not on screen yet (content-visibility: auto).'],];@@@ -57,7 +57,7 @@ function loadingPage({ base = 'https://openvibe.design' } = {}) {const body = `<article class="ld"><header class="ld-hero"><p class="ld-eye">Loading</p>-- <h1 class="ld-h1">Nothing spins. Everything has its shape.</h1>++ <h1 class="ld-h1">Everything has its shape. The wait is the mark’s own.</h1><p class="ld-lede">How OpenVibe waits: the page arrives drawn, a list shows its own shape until it fills in, the mark works while a page moves, and a slow load says so. The same parts on every site, from <code>openvibe-shared</code>.</p><nav class="ld-jump" aria-label="On this page"><a href="#shapes">Placeholders</a><a href="#mark">The mark at work</a><a href="#nav">Between pages</a><a href="#rules">The rules</a><a href="#use">Use it</a></nav></header>@@@ -103,8 +103,8 @@ function loadingPage({ base = 'https://openvibe.design' } = {}) {</section></article>`;return layout({-- title: 'Loading: nothing spins',-- description: 'How OpenVibe waits: placeholders shaped like the content, the mark at work instead of a spinner, the navbar while a page moves, and the rules. Live demos and the code, free.',++ title: 'Loading: everything has its shape',++ description: 'How OpenVibe waits: placeholders shaped like the content, the mark at work instead of a borrowed spinner, the navbar while a page moves, and the rules. Live demos and the code, free.',path: '/loading', active: '/loading', styles: ['css/icons.css', 'css/loading.css'], scripts: ['js/loading-page.js'], shared: ['loading.js'], body,});}@@@ -100,9 +100,9 @@ function motionPage({ base = 'https://openvibe.design' } = {}) {<div class="mo-sec-head"><h2 id="mo-rules">The rules</h2><p>So that motion stays a signal, never noise.</p></div><div class="mo-rules">${rule('It means something.', 'Every move says one thing: this is new, this worked, this is yours to press. A move that says nothing is cut.')}-- ${rule('Nothing spins.', 'A wait is a shimmer, a bob or a ring drawing round, never a circle chasing its tail.')}++ ${rule('The wait is the mark\u2019s own.', 'Working is its O as dots going round; thinking is three dots rising inside it. Nothing else turns a circle, and there is no borrowed spinner.')}${rule('Once, then still.', 'Effects play on a hover, a press or the first time something is seen, then rest. Nothing runs on its own, and nothing moves off screen.')}-- ${rule('Calm wins.', 'With reduced motion or Calm on, nothing moves, nothing counts and no confetti falls. The page looks the same, only still.')}++ ${rule('Calm is gentler. Off is still.', 'Calm, or a device that asks for less motion, plays every effect smaller and slower. Only Off stops them: then nothing moves, nothing counts and no confetti falls.')}${rule('Never hide to animate.', 'What is on screen when the page opens is never hidden for an entrance. Only what is further down rises in.')}${rule('One kit.', 'Every site uses the same effects and the same timing, so the network feels like one place.')}</div>@@@ -22,7 +22,7 @@ const { SHAPES } = require('../server/views/loading');assert.match(html, /\/shared\/loading\.js/, 'the shared loader');assert.match(html, /\/js\/loading-page\.js/);assert.ok(html.includes('data-ld-do="start"') && html.includes('data-ld-busy'));-- assert.ok(html.includes('Nothing spins.') && html.includes('content-visibility: auto'), 'the rules');++ assert.ok(html.includes('The wait is the mark\u2019s own.') && html.includes('content-visibility: auto'), 'the rules');assert.ok(!/fa-spin|class="[^"]*spinner/i.test(html), 'no spinner anywhere');});await check('the page script: Fill in measures the move, the loader runs its states, the navbar shows busy', async () => {@@@ -22,7 +22,7 @@ const { EFFECTS, CURVES, curvePath } = require('../server/views/motion');assert.ok(new RegExp(`<code>data-fx=("|"|")${e.fx}("|"|")</code>`).test(html), `${e.name}: the words to copy`);}assert.ok(/data-fx="[^"]*"/.test(html), 'the navbar loads ov-fx.js for this page');-- assert.ok(html.includes('Nothing spins.') && html.includes('Calm wins.') && html.includes('Never hide to animate.'), 'the rules');++ assert.ok(html.includes('The wait is the mark\u2019s own.') && html.includes('Calm is gentler. Off is still.') && html.includes('Never hide to animate.'), 'the rules');assert.ok(/\/shared\/ov-fx\.js("|") defer><\/script>/.test(html) && html.includes('OVFx.confetti'), 'the code');assert.ok(html.includes('css/motion.css') && html.includes('js/motion.js'), 'its own style and script');});
@@ -43,6 +43,42 @@ const RULES = Object.freeze([
@@ -43,6 +43,42 @@ const RULES = Object.freeze([
'Motion has four settings: Automatic (follows the device), Full, Calm and Off. Under Calm, and on a device that asks for less motion, every effect still plays: a third of the distance, no overshoot, endless loops at 0.6 of their speed. A page never stops an effect because motion is reduced; a rule that stops one belongs to Off (html[data-ov-motion=off]). A distance of 24px or more is multiplied by --ov-move. Only what starts by itself and fills the view (a video, a carousel) still waits for a tap under Calm.',
'Motion has four settings: Automatic (follows the device), Full, Calm and Off. Under Calm, and on a device that asks for less motion, every effect still plays: a third of the distance, no overshoot, endless loops at 0.6 of their speed. A page never stops an effect because motion is reduced; a rule that stops one belongs to Off (html[data-ov-motion=off]). A distance of 24px or more is multiplied by --ov-move. Only what starts by itself and fills the view (a video, a carousel) still waits for a tap under Calm.',
'2026-11-10', 'Reduced motion gave every animation no length at all, so Calm pages looked broken: the owner asked for motion reduced, not removed.',
'2026-11-10', 'Reduced motion gave every animation no length at all, so Calm pages looked broken: the owner asked for motion reduced, not removed.',
[{ label: 'Motion', href: '/motion' }, { label: 'Your look', href: '/look' }], ['openvibe-shared test: motion-setting']),
[{ label: 'Motion', href: '/motion' }, { label: 'Your look', href: '/look' }], ['openvibe-shared test: motion-setting']),
+ R('effects-by-device-not-screen', 'motion', 'An effect is left out by what the device says, never by the size of its screen.',
+ 'A site may leave out an effect a device cannot afford, by what the device says about itself: 2 GB of memory or less (navigator.deviceMemory), or a person saving data (navigator.connection.saveData). It never does so because the screen is narrow or the pointer is coarse, and never by overriding the person’s Motion setting. What follows a pointer (a spotlight, a tilt) may be left out where there is no pointer; a layout rule under a width (a collage that has no room) is not a motion rule.',
+ '2026-11-10', 'OpenVibe.Live marked every phone "lite" and switched its home’s animations off whatever the Motion setting said: on a phone nothing moved, and the owner asked why with motion on.',
+ [{ label: 'Motion', href: '/motion' }], ['OpenVibe.Live test: home-lite']),
+ R('a-dock-travels', 'motion', 'A control that docks moves to its place.',
+ 'An action that pins itself to the screen’s edge while its real control is out of view rises into place, and when the real control comes back into view the pinned one travels to it and hands over. It never appears and vanishes in one frame. Once the page’s footer is on screen it rides above the footer instead of lying over it. With Motion off it is simply there or not.',
+ '2026-11-10', 'The Go live bar on a phone flashed in and out as the page scrolled, and sat over the footer at the page’s end.',
+ [{ label: 'Motion', href: '/motion' }], ['OpenVibe.Live test: golive-setup']),
+ R('a-fold-shows-its-first', 'length', 'A fold shows its first two and fades.',
+ 'A list shortened on a phone shows its first two items whole and fades into the third, with one button under it that says how many there are, opens the rest and closes them again. A closed fold that shows only a heading and a count does not read as something to open, and what is in it cannot be seen. Every item stays in the page.',
+ '2026-11-10', 'Community’s Questions was a heading and "6 questions": the owner did not see that it opened, or a single question.',
+ [{ label: 'Patterns', href: '/patterns' }], ['OpenVibe.Community test: home-short']),
+ R('the-rail-names-its-places', 'navigation', 'A place is named, not initialled.',
+ 'A strip or rail of places a person switches between carries each place’s name: beside its mark where there is room (a phone’s strip is named chips), and as a label that follows the pointer where the rail is a column of tiles. Two initials in a circle are a mark, not a name. A place that can be managed opens its sheet when held, right-clicked, or tapped again while open: what it is, the person’s rank in it, and what that rank may do.',
+ '2026-11-10', 'Chat’s server strip was circles reading "O" and "T": the owner did not know what they were or how to remove one.',
+ [{ label: 'Patterns', href: '/patterns' }], ['OpenVibe.Chat test: shell']),
+ R('one-place-once', 'navigation', 'A destination is offered once on a screen.',
+ 'Where a phone has the bottom bar, a strip above the page does not repeat the bar’s places, and one icon means one thing on a site: the globe is not both "Servers" and "Global chat". A link in a bar goes to a page that exists.',
+ '2026-11-10', 'Chat showed Vibe at the top and at the bottom, used the globe for two places, and its Activity button opened a page that had never existed.',
+ [{ label: 'The Frame', href: '/system' }], ['OpenVibe.Chat test: shell']),
+ R('a-hero-does-not-name-the-site', 'words', 'A hero does not say the site’s name above its title.',
+ 'The bar already says which site this is. A line above a hero’s title is there only when it says something the bar does not: a state ("Live now"), a count, a date. A hero shows what the product does, with something real from it, before it describes it.',
+ '2026-11-10', 'openvibe.exchange opened with "OPENVIBE.EXCHANGE" over its title, under a bar that reads Exchange.',
+ [{ label: 'Patterns', href: '/patterns' }]),
+ R('a-handle-is-written-with-its-at', 'words', 'A link to an @address says @name.',
+ 'Where a row, a card or a link opens an /@name address and names it by the handle, the handle is written @name: the words are the address. A display name stays a display name, with the @handle on the second line where the row has one. Not in a chat line’s author and not in running text a person wrote.',
+ '2026-11-10', 'A profile listed "Maticus · Live channel" for a card that opens openvibe.live/@Maticus.',
+ [{ label: 'Patterns', href: '/patterns' }], ['OpenVibe.Network test: public-profile']),
+ R('a-computer-is-added-once', 'products', 'A computer is added in one place, and picked everywhere.',
+ 'A person adds a computer at openvibe.link. Every other site picks from the person’s Link machines with the one shared picker and never asks anyone to type a machine’s id. A site may show which machine it uses and what it is doing there; it keeps no list of its own, no "add a machine" of its own and no machines tab.',
+ '2026-11-10', 'Actor asked for a "Link machine ID" in a text field, beside other sites with machine pages of their own.',
+ [{ label: 'The system', href: '/system' }]),
+ R('one-composer', 'look', 'One composer, wherever a person writes.',
+ 'Every place a person writes to people or to an assistant uses the shared composer: one box with the text in it, Add on its left where the place takes files, what the place offers (tools, a model) in a row inside it, Send on its right, lit while there is something to send. What is being added waits in a tray above the text with its progress. A feature added to the composer arrives on every site; a site does not grow a composer of its own.',
+ '2026-11-10', 'Chat had four hand-written composers and a picture button outside the box; Actor and Vibe had two more shapes of the same thing.',
+ [{ label: 'Patterns', href: '/patterns' }], ['OpenVibe.Chat test: shell']),
R('count-what-it-says', 'honest', 'A number counts exactly what its label says.',
R('count-what-it-says', 'honest', 'A number counts exactly what its label says.',
'A count next to a list equals the length of that list. "4 agents working now" must count agents at work, not the agent catalogue.',
'A count next to a list equals the length of that list. "4 agents working now" must count agents at work, not the agent catalogue.',
'2026-11-01', 'A home page counted its catalogue as "working now".',
'2026-11-01', 'A home page counted its catalogue as "working now".',
@@ -92,9 +128,9 @@ const RULES = Object.freeze([
@@ -92,9 +128,9 @@ const RULES = Object.freeze([
'A setting has a single control, and an action a single button on the page. When a picture of the choices can be pressed, the pictures are the control: no second switch beside them saying the same thing.',
'A setting has a single control, and an action a single button on the page. When a picture of the choices can be pressed, the pictures are the control: no second switch beside them saying the same thing.',
'2026-11-09', 'Your look had the icon style as a row of three words and again as three screens; Themes asked people to sign in twice on one screen.',
'2026-11-09', 'Your look had the icon style as a row of three words and again as three screens; Themes asked people to sign in twice on one screen.',
[{ label: 'Your look', href: '/look' }]),
[{ label: 'Your look', href: '/look' }]),
- R('no-spinners', 'motion', 'No spinning rings. Things move only while something is actually happening.',
- 'Loading shows the shape of what is coming (a shimmer placeholder of the right size) or the mark at work. A finished or idle thing is still.',
- '2026-10-31', 'Spinning rings around icons everywhere looked cheap and said nothing.',
+ R('no-spinners', 'motion', 'The only thing that goes round is the mark\u2019s own O.',
+ 'A wait shows the shape of what is coming (a placeholder of the right size) or the mark at work: its O breaks into dots that go round, and three dots rise inside it while an agent thinks or writes. No borrowed spinner, and no ring chasing its tail round an icon. A finished or idle thing is still.',
+ '2026-11-10', 'Spinning rings round icons everywhere looked cheap and said nothing (2026-10-31). Then the mark only breathing did not read as loading, so its own O became the one thing that goes round.',
[{ label: 'Loading', href: '/loading' }, { label: 'The mark at work', href: '/patterns/mark-system' }], ['test/no-spin.test.js']),
[{ label: 'Loading', href: '/loading' }, { label: 'The mark at work', href: '/patterns/mark-system' }], ['test/no-spin.test.js']),
R('streams-hold-still', 'motion', 'A live list changes only where it changed.',
R('streams-hold-still', 'motion', 'A live list changes only where it changed.',
'Patch rows in place, keep the reader’s place, follow the bottom only when they were there, and fade in only what is new.',
'Patch rows in place, keep the reader’s place, follow the bottom only when they were there, and fade in only what is new.',
@@ -63,7 +63,8 @@ const live = (attrs = '', o = {}) => `<span class="ov-mark" ${attrs}>${art(o)}</
@@ -63,7 +63,8 @@ const live = (attrs = '', o = {}) => `<span class="ov-mark" ${attrs}>${art(o)}</
const STATES = [
const STATES = [
['', 'At rest', 'Still. It moves for a moment on your first visit, and when you point at it.'],
['', 'At rest', 'Still. It moves for a moment on your first visit, and when you point at it.'],
- ['busy', 'Working', 'The V bobs while the O breathes, in turn: something is on its way.'],
+ ['busy', 'Working', 'The O breaks into dots that go round while the V bobs: something is on its way.'],
+ ['think', 'Thinking', 'Three dots rise inside the O and the V glimmers: your agent is thinking or writing.'],
['ok', 'Done', 'The V becomes a tick and goes green. The tick only ever means done.'],
['ok', 'Done', 'The V becomes a tick and goes green. The tick only ever means done.'],
['error', 'Failed', 'The V becomes an exclamation mark, red, and the mark shakes once.'],
['error', 'Failed', 'The V becomes an exclamation mark, red, and the mark shakes once.'],
['no', 'Declined', 'The V becomes a cross, grey: cancelled, turned down, not now.'],
['no', 'Declined', 'The V becomes a cross, grey: cancelled, turned down, not now.'],
@@ -88,7 +89,7 @@ function brandPage({ base = 'https://openvibe.design' } = {}) {
@@ -88,7 +89,7 @@ function brandPage({ base = 'https://openvibe.design' } = {}) {
</header>`;
</header>`;
const moves = `<section class="br-sec" aria-labelledby="br-moves">
const moves = `<section class="br-sec" aria-labelledby="br-moves">
- <div class="br-sec-head"><h2 id="br-moves">It moves when it means something</h2><p>Every move is a word: one shape says working, done, failed and live, in colour and in form. Nothing spins.</p></div>
+ <div class="br-sec-head"><h2 id="br-moves">It moves when it means something</h2><p>Every move is a word: one shape says working, thinking, done, failed and live, in colour and in form. The only thing that goes round is its own O.</p></div>
<div class="br-play">
<div class="br-play">
<div class="br-play-stage">${live('data-size="190" data-wide data-br-demo', { wide: true })}</div>
<div class="br-play-stage">${live('data-size="190" data-wide data-br-demo', { wide: true })}</div>
<div class="br-play-side">
<div class="br-play-side">
@@ -49,7 +49,7 @@ function loadingPage({ base = 'https://openvibe.design' } = {}) {
@@ -49,7 +49,7 @@ function loadingPage({ base = 'https://openvibe.design' } = {}) {
['A list shows its shape at once.', 'Its placeholders are in the first HTML, the same size as what replaces them, so filling in moves nothing (each demo above measures it).'],
['A list shows its shape at once.', 'Its placeholders are in the first HTML, the same size as what replaces them, so filling in moves nothing (each demo above measures it).'],
['A picture sits on its own colour first.', 'Its dominant colour, or a tiny blurred copy, until it arrives. The first picture asks for itself first (fetchpriority="high"); the rest wait until they are near (loading="lazy").'],
['A picture sits on its own colour first.', 'Its dominant colour, or a tiny blurred copy, until it arrives. The first picture asks for itself first (fetchpriority="high"); the rest wait until they are near (loading="lazy").'],
['Moving between pages feels like one app.', 'The mark works in the navbar with a two-pixel line under it. Links are fetched when pointed at (speculation rules), and the navbar and app bar stay still while the page cross-fades (view transitions).'],
['Moving between pages feels like one app.', 'The mark works in the navbar with a two-pixel line under it. Links are fetched when pointed at (speculation rules), and the navbar and app bar stay still while the page cross-fades (view transitions).'],
- ['Nothing spins.', 'The mark bobs and breathes, a line fills. Calm and reduced motion keep everything still.'],
+ ['The wait is the mark\u2019s own.', 'Its O goes round as dots while it works, three dots rise inside it while an agent thinks, and a line fills when the length is known. Calm plays it slower; only motion off keeps it still.'],
['Slow says so.', 'After a few seconds a load explains itself and offers what to do. A failure says what happened and offers Retry, never an endless wait.'],
['Slow says so.', 'After a few seconds a load explains itself and offers what to do. A failure says what happened and offers Retry, never an endless wait.'],
['Off screen costs nothing.', 'Long lists skip the work for what is not on screen yet (content-visibility: auto).'],
['Off screen costs nothing.', 'Long lists skip the work for what is not on screen yet (content-visibility: auto).'],
];
];
@@ -57,7 +57,7 @@ function loadingPage({ base = 'https://openvibe.design' } = {}) {
@@ -57,7 +57,7 @@ function loadingPage({ base = 'https://openvibe.design' } = {}) {
const body = `<article class="ld">
const body = `<article class="ld">
<header class="ld-hero">
<header class="ld-hero">
<p class="ld-eye">Loading</p>
<p class="ld-eye">Loading</p>
- <h1 class="ld-h1">Nothing spins. Everything has its shape.</h1>
+ <h1 class="ld-h1">Everything has its shape. The wait is the mark’s own.</h1>
<p class="ld-lede">How OpenVibe waits: the page arrives drawn, a list shows its own shape until it fills in, the mark works while a page moves, and a slow load says so. The same parts on every site, from <code>openvibe-shared</code>.</p>
<p class="ld-lede">How OpenVibe waits: the page arrives drawn, a list shows its own shape until it fills in, the mark works while a page moves, and a slow load says so. The same parts on every site, from <code>openvibe-shared</code>.</p>
<nav class="ld-jump" aria-label="On this page"><a href="#shapes">Placeholders</a><a href="#mark">The mark at work</a><a href="#nav">Between pages</a><a href="#rules">The rules</a><a href="#use">Use it</a></nav>
<nav class="ld-jump" aria-label="On this page"><a href="#shapes">Placeholders</a><a href="#mark">The mark at work</a><a href="#nav">Between pages</a><a href="#rules">The rules</a><a href="#use">Use it</a></nav>
</header>
</header>
@@ -103,8 +103,8 @@ function loadingPage({ base = 'https://openvibe.design' } = {}) {
@@ -103,8 +103,8 @@ function loadingPage({ base = 'https://openvibe.design' } = {}) {
</section>
</section>
</article>`;
</article>`;
return layout({
return layout({
- title: 'Loading: nothing spins',
- description: 'How OpenVibe waits: placeholders shaped like the content, the mark at work instead of a spinner, the navbar while a page moves, and the rules. Live demos and the code, free.',
+ title: 'Loading: everything has its shape',
+ description: 'How OpenVibe waits: placeholders shaped like the content, the mark at work instead of a borrowed spinner, the navbar while a page moves, and the rules. Live demos and the code, free.',
path: '/loading', active: '/loading', styles: ['css/icons.css', 'css/loading.css'], scripts: ['js/loading-page.js'], shared: ['loading.js'], body,
path: '/loading', active: '/loading', styles: ['css/icons.css', 'css/loading.css'], scripts: ['js/loading-page.js'], shared: ['loading.js'], body,
});
});
}
}
@@ -100,9 +100,9 @@ function motionPage({ base = 'https://openvibe.design' } = {}) {
@@ -100,9 +100,9 @@ function motionPage({ base = 'https://openvibe.design' } = {}) {
<div class="mo-sec-head"><h2 id="mo-rules">The rules</h2><p>So that motion stays a signal, never noise.</p></div>
<div class="mo-sec-head"><h2 id="mo-rules">The rules</h2><p>So that motion stays a signal, never noise.</p></div>
<div class="mo-rules">
<div class="mo-rules">
${rule('It means something.', 'Every move says one thing: this is new, this worked, this is yours to press. A move that says nothing is cut.')}
${rule('It means something.', 'Every move says one thing: this is new, this worked, this is yours to press. A move that says nothing is cut.')}
- ${rule('Nothing spins.', 'A wait is a shimmer, a bob or a ring drawing round, never a circle chasing its tail.')}
+ ${rule('The wait is the mark\u2019s own.', 'Working is its O as dots going round; thinking is three dots rising inside it. Nothing else turns a circle, and there is no borrowed spinner.')}
${rule('Once, then still.', 'Effects play on a hover, a press or the first time something is seen, then rest. Nothing runs on its own, and nothing moves off screen.')}
${rule('Once, then still.', 'Effects play on a hover, a press or the first time something is seen, then rest. Nothing runs on its own, and nothing moves off screen.')}
- ${rule('Calm wins.', 'With reduced motion or Calm on, nothing moves, nothing counts and no confetti falls. The page looks the same, only still.')}
+ ${rule('Calm is gentler. Off is still.', 'Calm, or a device that asks for less motion, plays every effect smaller and slower. Only Off stops them: then nothing moves, nothing counts and no confetti falls.')}
${rule('Never hide to animate.', 'What is on screen when the page opens is never hidden for an entrance. Only what is further down rises in.')}
${rule('Never hide to animate.', 'What is on screen when the page opens is never hidden for an entrance. Only what is further down rises in.')}
${rule('One kit.', 'Every site uses the same effects and the same timing, so the network feels like one place.')}
${rule('One kit.', 'Every site uses the same effects and the same timing, so the network feels like one place.')}
</div>
</div>
@@ -22,7 +22,7 @@ const { SHAPES } = require('../server/views/loading');
@@ -22,7 +22,7 @@ const { SHAPES } = require('../server/views/loading');
assert.match(html, /\/shared\/loading\.js/, 'the shared loader');
assert.match(html, /\/shared\/loading\.js/, 'the shared loader');
assert.match(html, /\/js\/loading-page\.js/);
assert.match(html, /\/js\/loading-page\.js/);
assert.ok(html.includes('data-ld-do="start"') && html.includes('data-ld-busy'));
assert.ok(html.includes('data-ld-do="start"') && html.includes('data-ld-busy'));
- assert.ok(html.includes('Nothing spins.') && html.includes('content-visibility: auto'), 'the rules');
+ assert.ok(html.includes('The wait is the mark\u2019s own.') && html.includes('content-visibility: auto'), 'the rules');
assert.ok(!/fa-spin|class="[^"]*spinner/i.test(html), 'no spinner anywhere');
assert.ok(!/fa-spin|class="[^"]*spinner/i.test(html), 'no spinner anywhere');
});
});
await check('the page script: Fill in measures the move, the loader runs its states, the navbar shows busy', async () => {
await check('the page script: Fill in measures the move, the loader runs its states, the navbar shows busy', async () => {
@@ -22,7 +22,7 @@ const { EFFECTS, CURVES, curvePath } = require('../server/views/motion');
@@ -22,7 +22,7 @@ const { EFFECTS, CURVES, curvePath } = require('../server/views/motion');
assert.ok(new RegExp(`<code>data-fx=("|"|")${e.fx}("|"|")</code>`).test(html), `${e.name}: the words to copy`);
assert.ok(new RegExp(`<code>data-fx=("|"|")${e.fx}("|"|")</code>`).test(html), `${e.name}: the words to copy`);
}
}
assert.ok(/data-fx="[^"]*"/.test(html), 'the navbar loads ov-fx.js for this page');
assert.ok(/data-fx="[^"]*"/.test(html), 'the navbar loads ov-fx.js for this page');
- assert.ok(html.includes('Nothing spins.') && html.includes('Calm wins.') && html.includes('Never hide to animate.'), 'the rules');
+ assert.ok(html.includes('The wait is the mark\u2019s own.') && html.includes('Calm is gentler. Off is still.') && html.includes('Never hide to animate.'), 'the rules');
assert.ok(/\/shared\/ov-fx\.js("|") defer><\/script>/.test(html) && html.includes('OVFx.confetti'), 'the code');
assert.ok(/\/shared\/ov-fx\.js("|") defer><\/script>/.test(html) && html.includes('OVFx.confetti'), 'the code');
assert.ok(html.includes('css/motion.css') && html.includes('js/motion.js'), 'its own style and script');
assert.ok(html.includes('css/motion.css') && html.includes('js/motion.js'), 'its own style and script');
});
});