openvibers/openvibe.design Commits

Four patterns from the domain sweep (#106)

OP OpenVibers committed 12 days ago · eecdbc23ab · 3 files, +50 −0

Changes

ddiff --git a/public/css/patterns.css b/public/css/patterns.css
iindex 0dc5d84..9350670 100644
---- a/public/css/patterns.css
++++ b/public/css/patterns.css
@@@ -43,3 +43,24 @@
.watch-demo.is-watching .bottom-nav{transform:translateY(110%)}
.watch-demo.is-watching .watch-chat{padding-bottom:8px}
@media (prefers-reduced-motion:reduce){.watch-demo .bottom-nav{transition:none}}
++
++/* 2026-11-01 patterns: labelled cards, ask with a button, own places, mixed words. */
++.lcards-demo{max-width:280px}.lcards-table{border-collapse:collapse;width:100%}
++.lcards-table thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
++.lcards-table tbody{display:grid;gap:8px}.lcards-table tr{display:grid;grid-template-columns:1fr 1fr;gap:6px 12px;padding:10px 12px;border:1px solid var(--border);border-radius:12px;background:var(--bg-primary)}
++.lcards-table td{display:block;padding:0}.lcards-table td:first-child{grid-column:1/-1;font-weight:700}
++.lcards-table td:not(:first-child)::before{content:attr(data-label);display:block;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted)}
++.lcards-demo.broken .lcards-table td:not(:first-child)::before{content:none}
++.ask-demo{max-width:320px;padding:14px 16px;border:1px solid var(--border);border-radius:14px;background:var(--bg-primary)}.ask-demo p{margin:0}.ask-demo p+p{margin-top:12px}
++.ask-btn{display:inline-flex;align-items:center;min-height:40px;padding:0 16px;border-radius:999px;background:var(--accent);color:var(--on-accent,#fff);font-weight:700;text-decoration:none}
++.ask-demo.broken{display:flex;flex-wrap:wrap;gap:4px}.ask-demo.broken p+p{margin-top:0}.ask-demo.broken .ask-btn{display:inline;min-height:0;padding:0;background:none;color:var(--accent-light,var(--accent));font-weight:400;text-decoration:underline}
++.places-demo{position:relative;width:220px;height:220px;margin:auto}
++.places-core{position:absolute;inset:80px;display:grid;place-items:center;border-radius:50%;background:color-mix(in srgb,var(--accent) 22%,var(--bg-primary));font-weight:800}
++.places-ring{position:absolute;border-radius:50%;border:1px dashed color-mix(in srgb,var(--text-primary) 18%,transparent)}.places-ring.r1{inset:40px}.places-ring.r2{inset:6px}
++.places-node{position:absolute;left:50%;top:50%;padding:3px 9px;border-radius:999px;font-size:12px;font-weight:700;background:var(--bg-secondary);border:1px solid var(--border);transform:translate(-50%,-50%) rotate(calc(var(--i) * 72deg)) translateY(-78px) rotate(calc(var(--i) * -72deg))}
++.places-demo.broken .places-node{display:none}.places-demo.broken .places-core{font-size:0}.places-demo.broken .places-core::after{content:'OV';font-size:18px}
++.mixed-demo{display:grid;grid-template-columns:1fr 1fr;gap:10px}.mixed-panel{display:flex;flex-wrap:wrap;gap:6px;padding:14px;border-radius:12px}
++.mixed-panel.light{background:#fff;--panel-text:#1a1a2e}.mixed-panel.dark{background:#0a0f1c;--panel-text:#e6edf7}
++.mixed-word{padding:2px 8px;border-radius:999px;font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;border:1px solid var(--hue);color:color-mix(in srgb,var(--hue) 50%,var(--panel-text))}
++.mixed-demo.broken .mixed-word{color:var(--hue)}
++@media (prefers-reduced-motion: reduce){.places-node{transition:none}}
ddiff --git a/public/js/patterns.js b/public/js/patterns.js
iindex d3be2d1..6e50645 100644
---- a/public/js/patterns.js
++++ b/public/js/patterns.js
@@@ -142,6 +142,31 @@
const set = (on) => { screen.style.setProperty('--clear', on ? '52px' : '0px'); pill.hidden = false; say(on ? 'The pill says how much room it takes: toasts stack above it.' : 'Nobody says: the toast covers the pill.'); toast(); };
control('Clear the pill', () => set(true), true);
control('The bug', () => set(false));
++ } else if (id === 'labelled-cards') {
++ const box = node('div', 'lcards-demo'), table = node('table', 'lcards-table');
++ const head = ['City', 'Studio', '1 bedroom', '2 bedrooms'], rows = [['Huntsville', '$902', '$1,010', '$1,147'], ['Mobile', '$858', '$893', '$1,054']];
++ const thead = node('thead'), htr = node('tr'); head.forEach((h) => htr.append(node('th', '', h))); thead.append(htr);
++ const tbody = node('tbody'); rows.forEach((r) => { const tr = node('tr'); r.forEach((v, i) => { const td = node('td', '', v); td.dataset.label = head[i]; tr.append(td); }); tbody.append(tr); });
++ table.append(thead, tbody); box.append(table); widget.append(box);
++ control('Labelled cards', () => { box.classList.remove('broken'); say('Each value names its column; the city is the card\u2019s title.'); }, true);
++ control('The bug', () => { box.classList.add('broken'); say('The header row is hidden and the cells carry no labels: four bare prices.'); });
++ } else if (id === 'ask-with-a-button') {
++ const card = node('div', 'ask-demo'), p = node('p', '', 'Your watches belong to your OpenVibe account.'), cta = node('p'), a = node('a', 'ask-btn', 'Sign in with OpenVibe');
++ a.href = '#'; a.addEventListener('click', (e) => e.preventDefault()); cta.append(a); card.append(p, cta); widget.append(card);
++ control('With a button', () => { card.classList.remove('broken'); say('One sentence, then the button: the next step stands out.'); }, true);
++ control('The bug', () => { card.classList.add('broken'); say('A link inside the sentence: easy to miss, and it reads like any other link.'); });
++ } else if (id === 'own-places') {
++ const orb = node('div', 'places-demo'), core = node('span', 'places-core', 'Run');
++ const nodes = ['Pair', 'Submit', 'Sandbox', 'Stream', 'Meter'].map((label, i) => { const n = node('span', 'places-node', label); n.style.setProperty('--i', i); return n; });
++ orb.append(core, ...nodes, node('span', 'places-ring r1'), node('span', 'places-ring r2')); widget.append(orb);
++ control('Its own places', () => { orb.classList.remove('broken'); say('Pair, submit, sandbox, stream, meter: the picture says how a job runs here.'); }, true);
++ control('The bug', () => { orb.classList.add('broken'); say('The same rings on six homes: the picture says nothing about this site.'); });
++ } else if (id === 'mixed-words') {
++ const row = node('div', 'mixed-demo');
++ ['light', 'dark'].forEach((kind) => { const panel = node('div', `mixed-panel ${kind}`); [['Legendary', '#fbbf24'], ['Rare', '#38bdf8'], ['AI', '#a78bfa']].forEach(([w, hue]) => { const b = node('span', 'mixed-word', w); b.style.setProperty('--hue', hue); panel.append(b); }); row.append(panel); });
++ widget.append(row);
++ control('Mixed with the text', () => { row.classList.remove('broken'); say('Half the hue, half the text: readable on both.'); }, true);
++ control('The bug', () => { row.classList.add('broken'); say('The pure hue: fine on dark, 1.7:1 for gold on white.'); });
} else if (id === 'mark-system') {
const row=node('div','mark-demo'),mark=node('span','ov-mark','ov');row.append(mark,node('span','','OpenVibe'));widget.append(row);control('Loading state',()=>{mark.classList.toggle('loading');say(mark.classList.contains('loading')?'Loading state active.':'Loading state stopped.');});control('Change site',()=>{mark.textContent=mark.textContent==='ov'?'lv':'ov';mark.style.borderRadius=mark.textContent==='lv'?'50%':'16px';say('The mark morphs for the active site.');});control('Open five studies',()=>{window.location.href='/patterns/mark/studies';});
}
ddiff --git a/server/patterns/catalog.js b/server/patterns/catalog.js
iindex 4f96e38..4f6becf 100644
---- a/server/patterns/catalog.js
++++ b/server/patterns/catalog.js
@@@ -43,6 +43,10 @@ const patterns = [
['measure-at-rest','Layout that holds','Measure at rest', 'Read an element’s box only after its entrance animation has ended. getBoundingClientRect includes transforms, so an element measured mid-entrance looks smaller or further away than it is.', '2026-10-30 · A Dock card measured 362 × 506 instead of 400 × 560, because it was read during its scale-in (Shared 4.51.0).', 'Wait for the motion to settle before you measure.', t('durationBase','easingStandard')],
['drag-or-tap','Layout that holds','Drag or tap, never both', 'A press becomes a drag only after the pointer moves 6 px. Listen to the moves on the window, because a quick drag leaves the handle before then. Swallow the click that ends a drag, so a drag that began on a tab does not select it.', '2026-10-30 · A fast drag from a Dock tab never started (the moves left the head before 6 px), and a slow one would have changed tabs (Shared 4.51.0).', 'Small moves are taps; real moves are drags.', t('tap','durationFast')],
['floating-clears-toasts','Layout that holds','Floating surfaces clear the toasts', 'A floating control or card in a corner tells the page how much room it takes (a CSS variable). Toasts in that corner stack above it instead of covering it.', '2026-10-30 · The Dock’s pill and desktop toasts both lived in the bottom-right corner (Shared 4.51.0).', 'Two things in one corner never cover each other.', t('spaceMd','durationBase','easingStandard')],
++ ['labelled-cards','Layout that holds','Wide tables become labelled cards', 'On a phone, a table of three columns or more shows each row as a card: its first cell is the title and every other cell is named by its column. Only tables whose cells carry their column’s name (data-label) change; any other table stays a table and scrolls inside itself.', '2026-11-01 · Event names broke mid-word on a phone; then hand-built city tables lost their header row and showed four bare prices.', 'Nothing is left out, nothing scrolls sideways, and every value says what it is.', t('spaceMd','text','surface')],
++ ['ask-with-a-button','Elements','Ask with a button', 'A page that needs an account says what it is for in one sentence, then offers one primary button that signs in and comes straight back. Never a bare link inside the sentence.', '2026-11-01 · Ten sites asked with a text link, and pages without the shared stylesheet drew their button class as plain text.', 'The next step is the most visible thing on the page.', t('tap','accent','radiusPill')],
++ ['own-places','Layout that holds','The hero draws its own places', 'A home’s first picture says what this site is: an orbit of its own places, or its own live numbers, every node a link. A generic picture shared by many sites says nothing.', '2026-11-01 · Six homes drew the same rings around the mark, and Search left a tall empty band where its numbers belonged.', 'The first screen shows this site, not the network’s wallpaper.', t('durationSlow','easingStandard','accent')],
++ ['mixed-words','Elements','Coloured words mix with the text', 'A word in a hue (a rarity, a status, an AI label) is the hue mixed with the theme’s text, about half and half. It holds on dark and light themes; fills and borders keep the pure hue.', '2026-11-01 · Gold, violet and green words read at 1.4 to 2.5:1 on Daylight.', 'One rule for words in colour, on every theme.', t('text','accent','surface')],
['mark-system','Brand','The mark at work', 'Treat the mark as a working component: it shows a page on its way, work, done, failed and live in one shape, and moves only when it has something to say. The mark is the Pair (2026-10-31); its moves, forms and files are on /brand.', '2026-10-30 · A static logo could not communicate loading or adapt to a site identity.', 'The mark carries its identity through loading and site changes.', t('durationSlow','easingStandard','easingBack')],
].map(([slug,group,title,rule,bug,summary,tokens]) => ({ slug, group, title, rule, bug, summary, tokens: [...new Set([...tokens, 'tap', 'spaceMd', 'durationBase'])] }));