openvibers/openvibe.shared Commits
4.29.1: room.css (cards, rails and room accents out of kit.css: 18 sites' CSS budgets), motion.js/rooms.js/room.css served at /shared/, ring icons for the new rooms (#161)
Changes
@@@ -6,6 +6,29 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 4.29.1 — 2026-10-28++++- **`room.css`**: the room kit's cards, rails and room accents leave `kit.css` for a stylesheet of their own.++ - `kit.css` stays the controls only, so sites that use the kit's controls are not charged for components they++ never draw. 4.28.0 had made 18 sites' CSS budgets fail on the pin round.++ - Pages with `kit.card`, `kit.rail` or `sc.room()` load `room.css` after `kit.css`.++- **`motion.js`, `rooms.js` and `room.css` are served at `/shared/`** (they were missing from `files.js`'s lists in++ 4.28.0, so `/shared/motion.js` answered 404).++- **The new rooms have ring icons:** each room id resolves to the nearest existing glyph until it gets its own, so the++ menu never draws the fallback mark. The mapping:++ - domains: globe;++ - design: palette;++ - today: clock;++ - report: gauge;++ - surf and casino: games;++ - fun: quest;++ - meme: image;++ - exchange: trade;++ - cam: video;++ - tours: map;++ - vision: AI;++ - shoes: deals.++## 4.29.0 — 2026-10-28- **Every result, in the OpenVibe menu** (the Search Fabric, plan 2026-10-28 §4.5; Contracts 0.225.0@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.29.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.29.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.29.0",++ "version": "4.29.1","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -71,7 +71,7 @@ It renders the parts in the order every room shares (plan 2026-10-28 §3.3), wra| 7 | Questions, the rest of OpenVibe | |The page loads:--- `kit.css` and `showcase.css`;++- `kit.css`, `room.css` (cards, rails, the room's accent) and `showcase.css`;- `kit-web.js` for rails, menus and share;- `motion.js` for the ticker, the typed hints and the reveal.@@@ -227,6 +227,8 @@ A rail scrolls sideways and snaps:A card's menu inside a rail is placed on the screen, so the scroller never clips it. An empty rail is `''`.++Cards, rails and a room's accent are drawn by `room.css` (4.29.1), loaded after `kit.css` on the pages that use them, so a page with only the kit's controls does not pay for them.++### Rooms (4.28.0)`require('openvibe-shared/rooms')` (`window.OpenVibeRooms`) holds the fourteen rooms. Each has a neighbourhood, a host,@@@ -44,6 +44,8 @@ const BROWSER = Object.freeze(['search.js', // search everything from the navbar's magnifier, / or Ctrl+K; navbar.js loads it on first use'loading.js', // the one loading look: a poster-backed overlay with status and progress, and skeleton rows'agent-session.js', // a coding agent's session as people read it: steps grouped, live output, diffs, helpers, hand-offs++ 'motion.js', // reveal on scroll, number ticker, typed hints (4.28.0; the server writes the end state)++ 'rooms.js', // the rooms' accents, lines and motifs (OpenVibeRooms; 4.28.0)'openvibe-sw.js', // Web Push service worker]);@@@ -54,6 +56,7 @@ const STYLES = Object.freeze(['profile-panel.css', // the panel profile-panel.js draws'player.css', // player.js's controls'kit.css', // the OpenVibe Kit's look (kit.js / openvibe-shared/kit build the markup; docs/kit.md)++ 'room.css', // kit.card, kit.rail and a room's accent (4.29.1; load after kit.css where cards, rails or sc.room() are)]);const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name));@@@ -233,80 +233,14 @@ div.ovk-set-row { cursor: default; }.ovk-sheet-body { padding: 14px 16px calc(16px + env(safe-area-inset-bottom, 0px)); overflow-y: auto; }/* ── Phones (< 600 px): bottom sheets, full-width forms ── */--/* ── Rooms (rooms.js): the room's accent in the theme's mode. rooms.style(id) sets --room-dark/--room-light on .ov-room;-- * without them the theme's accent stays. --room-accent: icons, outlines, motifs; --room-strong: text on the page;-- * --room-fill and --room-on: a filled button and its text (dark text on a dark theme's bright fill, white on light). ── */--.ov-room { --room-accent: var(--ovk-accent); --room-strong: var(--ovk-accent); --room-fill: var(--ovk-fill); --room-on: var(--ovk-on-fill); }--.ov-room[style*="--room-dark"] { --room-accent: var(--room-dark); --room-strong: var(--room-dark); --room-fill: var(--room-dark); --room-on: #0b0d12; }--:root[data-theme-mode="light"] .ov-room[style*="--room-light"] { --room-accent: var(--room-light); --room-strong: var(--room-light); --room-fill: var(--room-light); --room-on: #fff; }----/* ── Cards (kit.card): one card for every kind of thing ── */--.ovk-card { position: relative; display: flex; flex-direction: column; gap: 10px; min-width: 0; color: var(--ovk-text); }--.ovk-card-media { position: relative; display: block; aspect-ratio: var(--ovk-ar, 16/9); border-radius: var(--ovk-radius); overflow: hidden; background: var(--ovk-panel-2); isolation: isolate; box-shadow: 0 0 0 1px color-mix(in srgb, var(--ovk-line) 70%, transparent) inset; }--.ovk-card-media > img, .ovk-card-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }--.ovk-card-media > img { transition: transform .5s cubic-bezier(.2, .7, .2, 1); }--.ovk-card-video { opacity: 0; transition: opacity .25s; }--.ovk-card.is-playing .ovk-card-video { opacity: 1; }--.ovk-card-noimg { position: absolute; inset: 0; display: grid; place-items: center; color: var(--room-accent, var(--ovk-accent)); background: radial-gradient(120% 90% at 28% 18%, color-mix(in srgb, var(--room-accent, var(--ovk-accent)) 24%, transparent), transparent 72%), var(--ovk-panel-2); }--.ovk-card-noimg .ovk-i { width: 30%; max-width: 60px; height: auto; aspect-ratio: 1; stroke-width: 1.5; }--.ovk-card-badge { position: absolute; top: 8px; left: 8px; z-index: 1; display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 6px; background: rgba(0, 0, 0, .74); color: #fff; font: 800 11.5px/1.5 var(--ovk-font); letter-spacing: .04em; }--.ovk-card-badge.is-live { background: #d4202b; }--.ovk-card-badge.is-live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: ovk-pulse 1.6s ease-in-out infinite; }--@keyframes ovk-pulse { 50% { opacity: .35; } }--.ovk-card-corner { position: absolute; right: 8px; bottom: 8px; z-index: 1; padding: 2px 7px; border-radius: 6px; background: rgba(0, 0, 0, .78); color: #fff; font: 700 12px/1.4 var(--ovk-font); font-variant-numeric: tabular-nums; transition: opacity .25s; }--.ovk-card-peek { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; padding: 34px 12px 10px; background: linear-gradient(transparent, rgba(0, 0, 0, .88)); color: #fff; font-size: 12.5px; line-height: 1.4; opacity: 0; transform: translateY(8px); transition: opacity .3s, transform .35s cubic-bezier(.2, .7, .2, 1); pointer-events: none; }--.ovk-card-body { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }--.ovk-card-body > .ovk-mark { flex: none; }--.ovk-card-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }--.ovk-card-title { margin: 0; font-size: 15px; font-weight: 700; line-height: 1.3; letter-spacing: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }--.ovk-card-link { color: inherit; text-decoration: none; transition: color .2s; }--.ovk-card-link::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: var(--ovk-radius); }--.ovk-card-link:focus-visible { outline: none; }--.ovk-card-link:focus-visible::after { outline: 2px solid var(--room-accent, var(--ovk-accent)); outline-offset: 3px; }--.ovk-card-meta { margin: 0; display: flex; flex-wrap: wrap; color: var(--ovk-muted); font-size: 13px; line-height: 1.45; min-width: 0; }--.ovk-card-meta > * + *::before { content: "·"; margin: 0 6px; }--.ovk-card-who { font-weight: 600; color: var(--text-secondary, var(--ovk-muted)); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }--.ovk-card .ovk-more { position: relative; z-index: 2; margin: -6px -8px 0 0; }--@media (hover: hover) {-- .ovk-card:hover .ovk-card-media > img, .ovk-card:focus-within .ovk-card-media > img { transform: scale(1.05); }-- .ovk-card:hover .ovk-card-peek, .ovk-card:focus-within .ovk-card-peek { opacity: 1; transform: none; }-- .ovk-card:hover .ovk-card-corner:has(~ .ovk-card-peek), .ovk-card:focus-within .ovk-card-corner:has(~ .ovk-card-peek) { opacity: 0; }-- .ovk-card:hover .ovk-card-link { color: var(--room-strong, var(--ovk-accent)); }-- .ovk-card .ovk-more:not([open]) > summary { opacity: 0; transition: opacity .15s; }-- .ovk-card:hover .ovk-more > summary, .ovk-card:focus-within .ovk-more > summary { opacity: 1; }--}----/* ── Rails (kit.rail): a row that scrolls sideways and snaps ── */--.ovk-rail { margin: 26px 0 0; min-width: 0; --ovk-rail-bleed: 0px; }--.ovk-rail-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 12px; }--.ovk-rail-head h2 { margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.01em; }--.ovk-rail-more { display: inline-flex; align-items: center; gap: 2px; min-height: 32px; color: var(--room-strong, var(--ovk-accent)); font-weight: 700; font-size: .9rem; text-decoration: none; white-space: nowrap; }--.ovk-rail-more:hover { text-decoration: underline; }--.ovk-rail-wrap { position: relative; }--.ovk-rail-track { list-style: none; margin: 0 calc(-1 * var(--ovk-rail-bleed)); padding: 2px var(--ovk-rail-bleed) 10px; display: grid; grid-auto-flow: column; grid-auto-columns: var(--ovk-rail-w); gap: 14px; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: var(--ovk-rail-bleed); overscroll-behavior-x: contain; scrollbar-width: none; }--.ovk-rail-track::-webkit-scrollbar { display: none; }--.ovk-rail-track > li { scroll-snap-align: start; min-width: 0; }--.ovk-rail--m { --ovk-rail-w: min(80%, 320px); }--.ovk-rail--s { --ovk-rail-w: min(44%, 200px); }--.ovk-rail--l { --ovk-rail-w: min(86%, 440px); }--.ovk-rail-btn { position: absolute; top: 34%; z-index: 3; width: 40px; height: 40px; margin-top: -20px; border-radius: 50%; display: grid; place-items: center; border: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); box-shadow: 0 6px 18px rgba(0, 0, 0, .3); cursor: pointer; font-size: 18px; }--.ovk-rail-btn:hover { border-color: var(--room-accent, var(--ovk-accent)); color: var(--room-strong, var(--ovk-accent)); }--.ovk-rail-btn:focus-visible { outline: 2px solid var(--room-accent, var(--ovk-accent)); outline-offset: 2px; }--.ovk-rail-btn[hidden] { display: none; }--.ovk-rail-prev { left: 6px; }--.ovk-rail-next { right: 6px; }--.ovk-pop.is-fixed { position: fixed; right: auto; top: auto; }@media (max-width: 599px) {-- .ovk-rail { --ovk-rail-bleed: 16px; }-- .ovk-rail-btn { display: none; }.ovk-sheet { margin: auto 0 0; width: 100vw; border-radius: 18px 18px 0 0; border-bottom: 0; }.ovk-row > a, .ovk-row--static { padding-left: 12px; }.ovk-head-tools { gap: 0; }}/* ── Tablets and up (≥ 600 px): numbers in a row, roomier lists ── */@media (min-width: 600px) {-- .ovk-rail--m { --ovk-rail-w: 280px; } .ovk-rail--s { --ovk-rail-w: 190px; } .ovk-rail--l { --ovk-rail-w: 400px; }.ovk-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); margin-left: 0; margin-right: 0; padding: 0; overflow: visible; }.ovk-save .ovk-btn { width: auto; min-width: 200px; }.ovk-dock { margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; }@@@ -317,6 +251,5 @@ div.ovk-set-row { cursor: default; }.ovk-split { grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }.ovk-split > .ovk-side { position: sticky; top: calc(var(--ovk-top) + 12px); }}--@media (prefers-reduced-motion: reduce) { .ovk-switch, .ovk-switch::after, .ovk-fold > summary > .ovk-i, .ovk-head, .ovk-btn, .ovk-card-media > img, .ovk-card-peek, .ovk-card-video { transition: none; } .ovk-card-badge.is-live::before { animation: none; } }--html[data-ov-motion=reduced] .ovk-card-badge.is-live::before { animation: none; }++@media (prefers-reduced-motion: reduce) { .ovk-switch, .ovk-switch::after, .ovk-fold > summary > .ovk-i, .ovk-head, .ovk-btn { transition: none; } }@media print { .ovk-head, .ovk-dock, .ovk-save { position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; } }@@@ -107,7 +107,9 @@};// [dx, dy] in glyph units that move each glyph's drawn bounds onto the centre of the ring. Generated.const OFFSETS = {"ov":[0,-0.5],"network":[0.56,-0.39],"live":[-0.01,-0.38],"tools":[0,-0.39],"media":[0,-0.39],"games":[0,-0.39],"community":[0,-0.39],"chat":[0,-0.39],"codes":[0,-0.39],"bot":[0,-0.39],"code":[0,-0.39],"blog":[0,-0.39],"wiki":[-0.01,-0.39],"news":[0,-0.39],"reviews":[-0.02,-0.39],"tips":[0,-0.39],"vip":[-0.01,-0.39],"trade":[0,-0.39],"host":[0,-0.39],"deals":[0.19,-0.21],"coupons":[-0.01,-0.39],"stream":[0,-0.39],"video":[-0.01,-0.39],"download":[0,-0.39],"upload":[0,-0.39],"image":[0,-0.39],"audio":[0,-0.38],"pdf":[0.28,-0.39],"docs":[0,-0.39],"text":[-0.01,-0.39],"logo":[-1.25,-0.5],"dns":[0,-0.39],"ip":[0,-0.37],"ssl":[-0.01,-0.39],"ping":[0,-0.39],"whois":[0,-0.39],"search":[0,-0.39],"map":[-0.01,-0.39],"food":[-0.01,-0.39],"gauge":[0,-0.39],"link":[0,-0.39],"shield":[0,-0.33],"plug":[0,-0.39],"work":[0,0.17],"rent":[0,-0.39],"help":[0,-0.39],"quest":[-0.01,-0.39],"inventory":[0,-0.37],"play":[0,-0.39],"drive":[0,-0.39],"services":[-0.01,-0.39],"space":[0,-0.39],"watch":[-0.02,-0.39],"actor":[0,-0.39],"events":[-0.01,-0.39],"ai":[0,-0.39],"run":[-0.01,-0.39],"billing":[0,-0.39],"feeds":[-0.01,-0.39],"pics":[-0.01,-0.39],"homes":[-0.01,-0.39],"website":[0,-0.39],"zone":[-0.01,-0.39],"fund":[-0.01,-0.39],"paste":[0,-0.39],"account":[-0.01,-0.39],"bell":[-0.01,-0.39],"history":[0,-0.39],"theme":[0,-0.39],"check":[-0.01,-0.39],"error":[0,-0.39],"clip":[0.13,-0.39]};-- const ALIASES = { yt: 'youtube', net: 'dns', dev: 'code', img: 'image', sound: 'audio', document: 'pdf', pastes: 'paste', openre: 'stream', maps: 'map', user: 'account', notifications: 'bell', 'media-hub': 'drive', mediahub: 'drive', sources: 'feeds', agent: 'actor', jobs: 'work', homes: 'homes', page: 'docs', db: 'zone', database: 'zone', location: 'ip', gamepad: 'games', robot: 'bot', deploy: 'upload', copy: 'paste', activity: 'ping' };++ const ALIASES = { yt: 'youtube', net: 'dns', dev: 'code', img: 'image', sound: 'audio', document: 'pdf', pastes: 'paste', openre: 'stream', maps: 'map', user: 'account', notifications: 'bell', 'media-hub': 'drive', mediahub: 'drive', sources: 'feeds', agent: 'actor', jobs: 'work', homes: 'homes', page: 'docs', db: 'zone', database: 'zone', location: 'ip', gamepad: 'games', robot: 'bot', deploy: 'upload', copy: 'paste', activity: 'ping',++ // The 2026-10-28 rooms, on the nearest glyph until each has its own (rooms.js): never the fallback mark.++ domains: 'dns', design: 'theme', today: 'history', report: 'gauge', surf: 'games', casino: 'games', fun: 'quest', meme: 'image', exchange: 'trade', cam: 'video', tours: 'map', vision: 'ai', shoes: 'deals' };const registry = Object.create(null);for (const [k, g] of Object.entries(GLYPHS)) registry[k] = { glyph: g };@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.29.0",++ "version": "4.29.1","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {nnew file mode 100644@@@ -0,0 +1,77 @@++/* openvibe-shared/room.css — the room kit's look (4.29.1; before, in kit.css): a room's accent (rooms.js), kit.card and++ * kit.rail. A page that shows cards or rails, or wears a room's accent (sc.room() does all three), loads it after++ * kit.css; pages that use only the kit's controls do not pay for it. Uses kit.css's tokens (--ovk-*). */++++/* ── Rooms (rooms.js): the room's accent in the theme's mode. rooms.style(id) sets --room-dark/--room-light on .ov-room;++ * without them the theme's accent stays. --room-accent: icons, outlines, motifs; --room-strong: text on the page;++ * --room-fill and --room-on: a filled button and its text (dark text on a dark theme's bright fill, white on light). ── */++.ov-room { --room-accent: var(--ovk-accent); --room-strong: var(--ovk-accent); --room-fill: var(--ovk-fill); --room-on: var(--ovk-on-fill); }++.ov-room[style*="--room-dark"] { --room-accent: var(--room-dark); --room-strong: var(--room-dark); --room-fill: var(--room-dark); --room-on: #0b0d12; }++:root[data-theme-mode="light"] .ov-room[style*="--room-light"] { --room-accent: var(--room-light); --room-strong: var(--room-light); --room-fill: var(--room-light); --room-on: #fff; }++++/* ── Cards (kit.card): one card for every kind of thing ── */++.ovk-card { position: relative; display: flex; flex-direction: column; gap: 10px; min-width: 0; color: var(--ovk-text); }++.ovk-card-media { position: relative; display: block; aspect-ratio: var(--ovk-ar, 16/9); border-radius: var(--ovk-radius); overflow: hidden; background: var(--ovk-panel-2); isolation: isolate; box-shadow: 0 0 0 1px color-mix(in srgb, var(--ovk-line) 70%, transparent) inset; }++.ovk-card-media > img, .ovk-card-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }++.ovk-card-media > img { transition: transform .5s cubic-bezier(.2, .7, .2, 1); }++.ovk-card-video { opacity: 0; transition: opacity .25s; }++.ovk-card.is-playing .ovk-card-video { opacity: 1; }++.ovk-card-noimg { position: absolute; inset: 0; display: grid; place-items: center; color: var(--room-accent, var(--ovk-accent)); background: radial-gradient(120% 90% at 28% 18%, color-mix(in srgb, var(--room-accent, var(--ovk-accent)) 24%, transparent), transparent 72%), var(--ovk-panel-2); }++.ovk-card-noimg .ovk-i { width: 30%; max-width: 60px; height: auto; aspect-ratio: 1; stroke-width: 1.5; }++.ovk-card-badge { position: absolute; top: 8px; left: 8px; z-index: 1; display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 6px; background: rgba(0, 0, 0, .74); color: #fff; font: 800 11.5px/1.5 var(--ovk-font); letter-spacing: .04em; }++.ovk-card-badge.is-live { background: #d4202b; }++.ovk-card-badge.is-live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: ovk-pulse 1.6s ease-in-out infinite; }++@keyframes ovk-pulse { 50% { opacity: .35; } }++.ovk-card-corner { position: absolute; right: 8px; bottom: 8px; z-index: 1; padding: 2px 7px; border-radius: 6px; background: rgba(0, 0, 0, .78); color: #fff; font: 700 12px/1.4 var(--ovk-font); font-variant-numeric: tabular-nums; transition: opacity .25s; }++.ovk-card-peek { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; padding: 34px 12px 10px; background: linear-gradient(transparent, rgba(0, 0, 0, .88)); color: #fff; font-size: 12.5px; line-height: 1.4; opacity: 0; transform: translateY(8px); transition: opacity .3s, transform .35s cubic-bezier(.2, .7, .2, 1); pointer-events: none; }++.ovk-card-body { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }++.ovk-card-body > .ovk-mark { flex: none; }++.ovk-card-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }++.ovk-card-title { margin: 0; font-size: 15px; font-weight: 700; line-height: 1.3; letter-spacing: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }++.ovk-card-link { color: inherit; text-decoration: none; transition: color .2s; }++.ovk-card-link::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: var(--ovk-radius); }++.ovk-card-link:focus-visible { outline: none; }++.ovk-card-link:focus-visible::after { outline: 2px solid var(--room-accent, var(--ovk-accent)); outline-offset: 3px; }++.ovk-card-meta { margin: 0; display: flex; flex-wrap: wrap; color: var(--ovk-muted); font-size: 13px; line-height: 1.45; min-width: 0; }++.ovk-card-meta > * + *::before { content: "·"; margin: 0 6px; }++.ovk-card-who { font-weight: 600; color: var(--text-secondary, var(--ovk-muted)); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.ovk-card .ovk-more { position: relative; z-index: 2; margin: -6px -8px 0 0; }++@media (hover: hover) {++ .ovk-card:hover .ovk-card-media > img, .ovk-card:focus-within .ovk-card-media > img { transform: scale(1.05); }++ .ovk-card:hover .ovk-card-peek, .ovk-card:focus-within .ovk-card-peek { opacity: 1; transform: none; }++ .ovk-card:hover .ovk-card-corner:has(~ .ovk-card-peek), .ovk-card:focus-within .ovk-card-corner:has(~ .ovk-card-peek) { opacity: 0; }++ .ovk-card:hover .ovk-card-link { color: var(--room-strong, var(--ovk-accent)); }++ .ovk-card .ovk-more:not([open]) > summary { opacity: 0; transition: opacity .15s; }++ .ovk-card:hover .ovk-more > summary, .ovk-card:focus-within .ovk-more > summary { opacity: 1; }++}++++/* ── Rails (kit.rail): a row that scrolls sideways and snaps ── */++.ovk-rail { margin: 26px 0 0; min-width: 0; --ovk-rail-bleed: 0px; }++.ovk-rail-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 12px; }++.ovk-rail-head h2 { margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.01em; }++.ovk-rail-more { display: inline-flex; align-items: center; gap: 2px; min-height: 32px; color: var(--room-strong, var(--ovk-accent)); font-weight: 700; font-size: .9rem; text-decoration: none; white-space: nowrap; }++.ovk-rail-more:hover { text-decoration: underline; }++.ovk-rail-wrap { position: relative; }++.ovk-rail-track { list-style: none; margin: 0 calc(-1 * var(--ovk-rail-bleed)); padding: 2px var(--ovk-rail-bleed) 10px; display: grid; grid-auto-flow: column; grid-auto-columns: var(--ovk-rail-w); gap: 14px; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: var(--ovk-rail-bleed); overscroll-behavior-x: contain; scrollbar-width: none; }++.ovk-rail-track::-webkit-scrollbar { display: none; }++.ovk-rail-track > li { scroll-snap-align: start; min-width: 0; }++.ovk-rail--m { --ovk-rail-w: min(80%, 320px); }++.ovk-rail--s { --ovk-rail-w: min(44%, 200px); }++.ovk-rail--l { --ovk-rail-w: min(86%, 440px); }++.ovk-rail-btn { position: absolute; top: 34%; z-index: 3; width: 40px; height: 40px; margin-top: -20px; border-radius: 50%; display: grid; place-items: center; border: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); box-shadow: 0 6px 18px rgba(0, 0, 0, .3); cursor: pointer; font-size: 18px; }++.ovk-rail-btn:hover { border-color: var(--room-accent, var(--ovk-accent)); color: var(--room-strong, var(--ovk-accent)); }++.ovk-rail-btn:focus-visible { outline: 2px solid var(--room-accent, var(--ovk-accent)); outline-offset: 2px; }++.ovk-rail-btn[hidden] { display: none; }++.ovk-rail-prev { left: 6px; }++.ovk-rail-next { right: 6px; }++.ovk-pop.is-fixed { position: fixed; right: auto; top: auto; }++++@media (max-width: 599px) {++ .ovk-rail { --ovk-rail-bleed: 16px; }++ .ovk-rail-btn { display: none; }++}++@media (min-width: 600px) {++ .ovk-rail--m { --ovk-rail-w: 280px; } .ovk-rail--s { --ovk-rail-w: 190px; } .ovk-rail--l { --ovk-rail-w: 400px; }++}++@media (prefers-reduced-motion: reduce) { .ovk-card-media > img, .ovk-card-peek, .ovk-card-video { transition: none; } .ovk-card-badge.is-live::before { animation: none; } }++html[data-ov-motion=reduced] .ovk-card-badge.is-live::before { animation: none; }@@@ -354,7 +354,7 @@ function roomMotif(r) {* scenes: [{ title, text, html (trusted: the room's own component in a demo state), action: { href, label } }]* explore: { title, items: [{ href, title, text, icon }] } — sc.features · dev: sc.code's arguments, with links* faq: { title, items: [[q, a]] } · network: sc.network's arguments · html: trusted markup after the scenes-- * Needs kit.css (cards, rails, the room's accent) and showcase.css; motion.js makes the numbers tick and the box type.++ * Needs kit.css, room.css (cards, rails, the room's accent) and showcase.css; motion.js makes the numbers tick and the box type.*/function room({ room: key, hero: h = {}, proof, rails, scenes, explore, dev, faq: q, network: net, html } = {}) {roomsLib = roomsLib || require('./rooms');@@@ -98,9 +98,13 @@ const sc = require('../showcase');// ── The CSS is served and carries the room accent rules; motion.js keeps the server's end state ──{++ const rcss = fs.readFileSync(path.join(__dirname, '..', 'room.css'), 'utf8');const kcss = fs.readFileSync(path.join(__dirname, '..', 'kit.css'), 'utf8');-- assert.ok(kcss.includes(':root[data-theme-mode="light"] .ov-room[style*="--room-light"]'), 'light themes take the light accent');-- assert.ok(kcss.includes('.ovk-rail-btn { display: none; }'), 'phones swipe rails');++ assert.ok(rcss.includes(':root[data-theme-mode="light"] .ov-room[style*="--room-light"]'), 'light themes take the light accent');++ assert.ok(rcss.includes('.ovk-rail-btn { display: none; }'), 'phones swipe rails');++ assert.ok(!/ovk-card|ovk-rail|ov-room/.test(kcss), 'kit.css stays the controls only: cards, rails and accents are room.css (4.29.1)');++ const files = require('../files');++ for (const f of ['motion.js', 'rooms.js', 'room.css']) assert.ok(files.isBrowserFile(f), `${f} is served at /shared/`);const scss = fs.readFileSync(path.join(__dirname, '..', 'showcase.css'), 'utf8');assert.ok(scss.includes('.sc-room-hero{') && scss.includes('html.ov-motion [data-ov-reveal]'), 'showcase.css is rebuilt (npm run build:styles)');const motion = fs.readFileSync(path.join(__dirname, '..', 'motion.js'), 'utf8');
@@ -6,6 +6,29 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,29 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 4.29.1 — 2026-10-28
+
+- **`room.css`**: the room kit's cards, rails and room accents leave `kit.css` for a stylesheet of their own.
+ - `kit.css` stays the controls only, so sites that use the kit's controls are not charged for components they
+ never draw. 4.28.0 had made 18 sites' CSS budgets fail on the pin round.
+ - Pages with `kit.card`, `kit.rail` or `sc.room()` load `room.css` after `kit.css`.
+- **`motion.js`, `rooms.js` and `room.css` are served at `/shared/`** (they were missing from `files.js`'s lists in
+ 4.28.0, so `/shared/motion.js` answered 404).
+- **The new rooms have ring icons:** each room id resolves to the nearest existing glyph until it gets its own, so the
+ menu never draws the fallback mark. The mapping:
+ - domains: globe;
+ - design: palette;
+ - today: clock;
+ - report: gauge;
+ - surf and casino: games;
+ - fun: quest;
+ - meme: image;
+ - exchange: trade;
+ - cam: video;
+ - tours: map;
+ - vision: AI;
+ - shoes: deals.
+
## 4.29.0 — 2026-10-28
## 4.29.0 — 2026-10-28
- **Every result, in the OpenVibe menu** (the Search Fabric, plan 2026-10-28 §4.5; Contracts 0.225.0
- **Every result, in the OpenVibe menu** (the Search Fabric, plan 2026-10-28 §4.5; Contracts 0.225.0
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.29.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.29.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.29.0",
+ "version": "4.29.1",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -71,7 +71,7 @@ It renders the parts in the order every room shares (plan 2026-10-28 §3.3), wra
@@ -71,7 +71,7 @@ It renders the parts in the order every room shares (plan 2026-10-28 §3.3), wra
| 7 | Questions, the rest of OpenVibe | |
| 7 | Questions, the rest of OpenVibe | |
The page loads:
The page loads:
-- `kit.css` and `showcase.css`;
+- `kit.css`, `room.css` (cards, rails, the room's accent) and `showcase.css`;
- `kit-web.js` for rails, menus and share;
- `kit-web.js` for rails, menus and share;
- `motion.js` for the ticker, the typed hints and the reveal.
- `motion.js` for the ticker, the typed hints and the reveal.
@@ -227,6 +227,8 @@ A rail scrolls sideways and snaps:
@@ -227,6 +227,8 @@ A rail scrolls sideways and snaps:
A card's menu inside a rail is placed on the screen, so the scroller never clips it. An empty rail is `''`.
A card's menu inside a rail is placed on the screen, so the scroller never clips it. An empty rail is `''`.
+Cards, rails and a room's accent are drawn by `room.css` (4.29.1), loaded after `kit.css` on the pages that use them, so a page with only the kit's controls does not pay for them.
+
### Rooms (4.28.0)
### Rooms (4.28.0)
`require('openvibe-shared/rooms')` (`window.OpenVibeRooms`) holds the fourteen rooms. Each has a neighbourhood, a host,
`require('openvibe-shared/rooms')` (`window.OpenVibeRooms`) holds the fourteen rooms. Each has a neighbourhood, a host,
@@ -44,6 +44,8 @@ const BROWSER = Object.freeze([
@@ -44,6 +44,8 @@ const BROWSER = Object.freeze([
'search.js', // search everything from the navbar's magnifier, / or Ctrl+K; navbar.js loads it on first use
'search.js', // search everything from the navbar's magnifier, / or Ctrl+K; navbar.js loads it on first use
'loading.js', // the one loading look: a poster-backed overlay with status and progress, and skeleton rows
'loading.js', // the one loading look: a poster-backed overlay with status and progress, and skeleton rows
'agent-session.js', // a coding agent's session as people read it: steps grouped, live output, diffs, helpers, hand-offs
'agent-session.js', // a coding agent's session as people read it: steps grouped, live output, diffs, helpers, hand-offs
+ 'motion.js', // reveal on scroll, number ticker, typed hints (4.28.0; the server writes the end state)
+ 'rooms.js', // the rooms' accents, lines and motifs (OpenVibeRooms; 4.28.0)
'openvibe-sw.js', // Web Push service worker
'openvibe-sw.js', // Web Push service worker
]);
]);
@@ -54,6 +56,7 @@ const STYLES = Object.freeze([
@@ -54,6 +56,7 @@ const STYLES = Object.freeze([
'profile-panel.css', // the panel profile-panel.js draws
'profile-panel.css', // the panel profile-panel.js draws
'player.css', // player.js's controls
'player.css', // player.js's controls
'kit.css', // the OpenVibe Kit's look (kit.js / openvibe-shared/kit build the markup; docs/kit.md)
'kit.css', // the OpenVibe Kit's look (kit.js / openvibe-shared/kit build the markup; docs/kit.md)
+ 'room.css', // kit.card, kit.rail and a room's accent (4.29.1; load after kit.css where cards, rails or sc.room() are)
]);
]);
const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name));
const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name));
@@ -233,80 +233,14 @@ div.ovk-set-row { cursor: default; }
@@ -233,80 +233,14 @@ div.ovk-set-row { cursor: default; }
.ovk-sheet-body { padding: 14px 16px calc(16px + env(safe-area-inset-bottom, 0px)); overflow-y: auto; }
.ovk-sheet-body { padding: 14px 16px calc(16px + env(safe-area-inset-bottom, 0px)); overflow-y: auto; }
/* ── Phones (< 600 px): bottom sheets, full-width forms ── */
/* ── Phones (< 600 px): bottom sheets, full-width forms ── */
-/* ── Rooms (rooms.js): the room's accent in the theme's mode. rooms.style(id) sets --room-dark/--room-light on .ov-room;
- * without them the theme's accent stays. --room-accent: icons, outlines, motifs; --room-strong: text on the page;
- * --room-fill and --room-on: a filled button and its text (dark text on a dark theme's bright fill, white on light). ── */
-.ov-room { --room-accent: var(--ovk-accent); --room-strong: var(--ovk-accent); --room-fill: var(--ovk-fill); --room-on: var(--ovk-on-fill); }
-.ov-room[style*="--room-dark"] { --room-accent: var(--room-dark); --room-strong: var(--room-dark); --room-fill: var(--room-dark); --room-on: #0b0d12; }
-:root[data-theme-mode="light"] .ov-room[style*="--room-light"] { --room-accent: var(--room-light); --room-strong: var(--room-light); --room-fill: var(--room-light); --room-on: #fff; }
-
-/* ── Cards (kit.card): one card for every kind of thing ── */
-.ovk-card { position: relative; display: flex; flex-direction: column; gap: 10px; min-width: 0; color: var(--ovk-text); }
-.ovk-card-media { position: relative; display: block; aspect-ratio: var(--ovk-ar, 16/9); border-radius: var(--ovk-radius); overflow: hidden; background: var(--ovk-panel-2); isolation: isolate; box-shadow: 0 0 0 1px color-mix(in srgb, var(--ovk-line) 70%, transparent) inset; }
-.ovk-card-media > img, .ovk-card-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
-.ovk-card-media > img { transition: transform .5s cubic-bezier(.2, .7, .2, 1); }
-.ovk-card-video { opacity: 0; transition: opacity .25s; }
-.ovk-card.is-playing .ovk-card-video { opacity: 1; }
-.ovk-card-noimg { position: absolute; inset: 0; display: grid; place-items: center; color: var(--room-accent, var(--ovk-accent)); background: radial-gradient(120% 90% at 28% 18%, color-mix(in srgb, var(--room-accent, var(--ovk-accent)) 24%, transparent), transparent 72%), var(--ovk-panel-2); }
-.ovk-card-noimg .ovk-i { width: 30%; max-width: 60px; height: auto; aspect-ratio: 1; stroke-width: 1.5; }
-.ovk-card-badge { position: absolute; top: 8px; left: 8px; z-index: 1; display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 6px; background: rgba(0, 0, 0, .74); color: #fff; font: 800 11.5px/1.5 var(--ovk-font); letter-spacing: .04em; }
-.ovk-card-badge.is-live { background: #d4202b; }
-.ovk-card-badge.is-live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: ovk-pulse 1.6s ease-in-out infinite; }
-@keyframes ovk-pulse { 50% { opacity: .35; } }
-.ovk-card-corner { position: absolute; right: 8px; bottom: 8px; z-index: 1; padding: 2px 7px; border-radius: 6px; background: rgba(0, 0, 0, .78); color: #fff; font: 700 12px/1.4 var(--ovk-font); font-variant-numeric: tabular-nums; transition: opacity .25s; }
-.ovk-card-peek { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; padding: 34px 12px 10px; background: linear-gradient(transparent, rgba(0, 0, 0, .88)); color: #fff; font-size: 12.5px; line-height: 1.4; opacity: 0; transform: translateY(8px); transition: opacity .3s, transform .35s cubic-bezier(.2, .7, .2, 1); pointer-events: none; }
-.ovk-card-body { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }
-.ovk-card-body > .ovk-mark { flex: none; }
-.ovk-card-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
-.ovk-card-title { margin: 0; font-size: 15px; font-weight: 700; line-height: 1.3; letter-spacing: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
-.ovk-card-link { color: inherit; text-decoration: none; transition: color .2s; }
-.ovk-card-link::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: var(--ovk-radius); }
-.ovk-card-link:focus-visible { outline: none; }
-.ovk-card-link:focus-visible::after { outline: 2px solid var(--room-accent, var(--ovk-accent)); outline-offset: 3px; }
-.ovk-card-meta { margin: 0; display: flex; flex-wrap: wrap; color: var(--ovk-muted); font-size: 13px; line-height: 1.45; min-width: 0; }
-.ovk-card-meta > * + *::before { content: "·"; margin: 0 6px; }
-.ovk-card-who { font-weight: 600; color: var(--text-secondary, var(--ovk-muted)); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.ovk-card .ovk-more { position: relative; z-index: 2; margin: -6px -8px 0 0; }
-@media (hover: hover) {
- .ovk-card:hover .ovk-card-media > img, .ovk-card:focus-within .ovk-card-media > img { transform: scale(1.05); }
- .ovk-card:hover .ovk-card-peek, .ovk-card:focus-within .ovk-card-peek { opacity: 1; transform: none; }
- .ovk-card:hover .ovk-card-corner:has(~ .ovk-card-peek), .ovk-card:focus-within .ovk-card-corner:has(~ .ovk-card-peek) { opacity: 0; }
- .ovk-card:hover .ovk-card-link { color: var(--room-strong, var(--ovk-accent)); }
- .ovk-card .ovk-more:not([open]) > summary { opacity: 0; transition: opacity .15s; }
- .ovk-card:hover .ovk-more > summary, .ovk-card:focus-within .ovk-more > summary { opacity: 1; }
-}
-
-/* ── Rails (kit.rail): a row that scrolls sideways and snaps ── */
-.ovk-rail { margin: 26px 0 0; min-width: 0; --ovk-rail-bleed: 0px; }
-.ovk-rail-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 12px; }
-.ovk-rail-head h2 { margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.01em; }
-.ovk-rail-more { display: inline-flex; align-items: center; gap: 2px; min-height: 32px; color: var(--room-strong, var(--ovk-accent)); font-weight: 700; font-size: .9rem; text-decoration: none; white-space: nowrap; }
-.ovk-rail-more:hover { text-decoration: underline; }
-.ovk-rail-wrap { position: relative; }
-.ovk-rail-track { list-style: none; margin: 0 calc(-1 * var(--ovk-rail-bleed)); padding: 2px var(--ovk-rail-bleed) 10px; display: grid; grid-auto-flow: column; grid-auto-columns: var(--ovk-rail-w); gap: 14px; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: var(--ovk-rail-bleed); overscroll-behavior-x: contain; scrollbar-width: none; }
-.ovk-rail-track::-webkit-scrollbar { display: none; }
-.ovk-rail-track > li { scroll-snap-align: start; min-width: 0; }
-.ovk-rail--m { --ovk-rail-w: min(80%, 320px); }
-.ovk-rail--s { --ovk-rail-w: min(44%, 200px); }
-.ovk-rail--l { --ovk-rail-w: min(86%, 440px); }
-.ovk-rail-btn { position: absolute; top: 34%; z-index: 3; width: 40px; height: 40px; margin-top: -20px; border-radius: 50%; display: grid; place-items: center; border: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); box-shadow: 0 6px 18px rgba(0, 0, 0, .3); cursor: pointer; font-size: 18px; }
-.ovk-rail-btn:hover { border-color: var(--room-accent, var(--ovk-accent)); color: var(--room-strong, var(--ovk-accent)); }
-.ovk-rail-btn:focus-visible { outline: 2px solid var(--room-accent, var(--ovk-accent)); outline-offset: 2px; }
-.ovk-rail-btn[hidden] { display: none; }
-.ovk-rail-prev { left: 6px; }
-.ovk-rail-next { right: 6px; }
-.ovk-pop.is-fixed { position: fixed; right: auto; top: auto; }
@media (max-width: 599px) {
@media (max-width: 599px) {
- .ovk-rail { --ovk-rail-bleed: 16px; }
- .ovk-rail-btn { display: none; }
.ovk-sheet { margin: auto 0 0; width: 100vw; border-radius: 18px 18px 0 0; border-bottom: 0; }
.ovk-sheet { margin: auto 0 0; width: 100vw; border-radius: 18px 18px 0 0; border-bottom: 0; }
.ovk-row > a, .ovk-row--static { padding-left: 12px; }
.ovk-row > a, .ovk-row--static { padding-left: 12px; }
.ovk-head-tools { gap: 0; }
.ovk-head-tools { gap: 0; }
}
}
/* ── Tablets and up (≥ 600 px): numbers in a row, roomier lists ── */
/* ── Tablets and up (≥ 600 px): numbers in a row, roomier lists ── */
@media (min-width: 600px) {
@media (min-width: 600px) {
- .ovk-rail--m { --ovk-rail-w: 280px; } .ovk-rail--s { --ovk-rail-w: 190px; } .ovk-rail--l { --ovk-rail-w: 400px; }
.ovk-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); margin-left: 0; margin-right: 0; padding: 0; overflow: visible; }
.ovk-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); margin-left: 0; margin-right: 0; padding: 0; overflow: visible; }
.ovk-save .ovk-btn { width: auto; min-width: 200px; }
.ovk-save .ovk-btn { width: auto; min-width: 200px; }
.ovk-dock { margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; }
.ovk-dock { margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; }
@@ -317,6 +251,5 @@ div.ovk-set-row { cursor: default; }
@@ -317,6 +251,5 @@ div.ovk-set-row { cursor: default; }
.ovk-split { grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }
.ovk-split { grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }
.ovk-split > .ovk-side { position: sticky; top: calc(var(--ovk-top) + 12px); }
.ovk-split > .ovk-side { position: sticky; top: calc(var(--ovk-top) + 12px); }
}
}
-@media (prefers-reduced-motion: reduce) { .ovk-switch, .ovk-switch::after, .ovk-fold > summary > .ovk-i, .ovk-head, .ovk-btn, .ovk-card-media > img, .ovk-card-peek, .ovk-card-video { transition: none; } .ovk-card-badge.is-live::before { animation: none; } }
-html[data-ov-motion=reduced] .ovk-card-badge.is-live::before { animation: none; }
+@media (prefers-reduced-motion: reduce) { .ovk-switch, .ovk-switch::after, .ovk-fold > summary > .ovk-i, .ovk-head, .ovk-btn { transition: none; } }
@media print { .ovk-head, .ovk-dock, .ovk-save { position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; } }
@media print { .ovk-head, .ovk-dock, .ovk-save { position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; } }
@@ -107,7 +107,9 @@
@@ -107,7 +107,9 @@
};
};
// [dx, dy] in glyph units that move each glyph's drawn bounds onto the centre of the ring. Generated.
// [dx, dy] in glyph units that move each glyph's drawn bounds onto the centre of the ring. Generated.
const OFFSETS = {"ov":[0,-0.5],"network":[0.56,-0.39],"live":[-0.01,-0.38],"tools":[0,-0.39],"media":[0,-0.39],"games":[0,-0.39],"community":[0,-0.39],"chat":[0,-0.39],"codes":[0,-0.39],"bot":[0,-0.39],"code":[0,-0.39],"blog":[0,-0.39],"wiki":[-0.01,-0.39],"news":[0,-0.39],"reviews":[-0.02,-0.39],"tips":[0,-0.39],"vip":[-0.01,-0.39],"trade":[0,-0.39],"host":[0,-0.39],"deals":[0.19,-0.21],"coupons":[-0.01,-0.39],"stream":[0,-0.39],"video":[-0.01,-0.39],"download":[0,-0.39],"upload":[0,-0.39],"image":[0,-0.39],"audio":[0,-0.38],"pdf":[0.28,-0.39],"docs":[0,-0.39],"text":[-0.01,-0.39],"logo":[-1.25,-0.5],"dns":[0,-0.39],"ip":[0,-0.37],"ssl":[-0.01,-0.39],"ping":[0,-0.39],"whois":[0,-0.39],"search":[0,-0.39],"map":[-0.01,-0.39],"food":[-0.01,-0.39],"gauge":[0,-0.39],"link":[0,-0.39],"shield":[0,-0.33],"plug":[0,-0.39],"work":[0,0.17],"rent":[0,-0.39],"help":[0,-0.39],"quest":[-0.01,-0.39],"inventory":[0,-0.37],"play":[0,-0.39],"drive":[0,-0.39],"services":[-0.01,-0.39],"space":[0,-0.39],"watch":[-0.02,-0.39],"actor":[0,-0.39],"events":[-0.01,-0.39],"ai":[0,-0.39],"run":[-0.01,-0.39],"billing":[0,-0.39],"feeds":[-0.01,-0.39],"pics":[-0.01,-0.39],"homes":[-0.01,-0.39],"website":[0,-0.39],"zone":[-0.01,-0.39],"fund":[-0.01,-0.39],"paste":[0,-0.39],"account":[-0.01,-0.39],"bell":[-0.01,-0.39],"history":[0,-0.39],"theme":[0,-0.39],"check":[-0.01,-0.39],"error":[0,-0.39],"clip":[0.13,-0.39]};
const OFFSETS = {"ov":[0,-0.5],"network":[0.56,-0.39],"live":[-0.01,-0.38],"tools":[0,-0.39],"media":[0,-0.39],"games":[0,-0.39],"community":[0,-0.39],"chat":[0,-0.39],"codes":[0,-0.39],"bot":[0,-0.39],"code":[0,-0.39],"blog":[0,-0.39],"wiki":[-0.01,-0.39],"news":[0,-0.39],"reviews":[-0.02,-0.39],"tips":[0,-0.39],"vip":[-0.01,-0.39],"trade":[0,-0.39],"host":[0,-0.39],"deals":[0.19,-0.21],"coupons":[-0.01,-0.39],"stream":[0,-0.39],"video":[-0.01,-0.39],"download":[0,-0.39],"upload":[0,-0.39],"image":[0,-0.39],"audio":[0,-0.38],"pdf":[0.28,-0.39],"docs":[0,-0.39],"text":[-0.01,-0.39],"logo":[-1.25,-0.5],"dns":[0,-0.39],"ip":[0,-0.37],"ssl":[-0.01,-0.39],"ping":[0,-0.39],"whois":[0,-0.39],"search":[0,-0.39],"map":[-0.01,-0.39],"food":[-0.01,-0.39],"gauge":[0,-0.39],"link":[0,-0.39],"shield":[0,-0.33],"plug":[0,-0.39],"work":[0,0.17],"rent":[0,-0.39],"help":[0,-0.39],"quest":[-0.01,-0.39],"inventory":[0,-0.37],"play":[0,-0.39],"drive":[0,-0.39],"services":[-0.01,-0.39],"space":[0,-0.39],"watch":[-0.02,-0.39],"actor":[0,-0.39],"events":[-0.01,-0.39],"ai":[0,-0.39],"run":[-0.01,-0.39],"billing":[0,-0.39],"feeds":[-0.01,-0.39],"pics":[-0.01,-0.39],"homes":[-0.01,-0.39],"website":[0,-0.39],"zone":[-0.01,-0.39],"fund":[-0.01,-0.39],"paste":[0,-0.39],"account":[-0.01,-0.39],"bell":[-0.01,-0.39],"history":[0,-0.39],"theme":[0,-0.39],"check":[-0.01,-0.39],"error":[0,-0.39],"clip":[0.13,-0.39]};
- const ALIASES = { yt: 'youtube', net: 'dns', dev: 'code', img: 'image', sound: 'audio', document: 'pdf', pastes: 'paste', openre: 'stream', maps: 'map', user: 'account', notifications: 'bell', 'media-hub': 'drive', mediahub: 'drive', sources: 'feeds', agent: 'actor', jobs: 'work', homes: 'homes', page: 'docs', db: 'zone', database: 'zone', location: 'ip', gamepad: 'games', robot: 'bot', deploy: 'upload', copy: 'paste', activity: 'ping' };
+ const ALIASES = { yt: 'youtube', net: 'dns', dev: 'code', img: 'image', sound: 'audio', document: 'pdf', pastes: 'paste', openre: 'stream', maps: 'map', user: 'account', notifications: 'bell', 'media-hub': 'drive', mediahub: 'drive', sources: 'feeds', agent: 'actor', jobs: 'work', homes: 'homes', page: 'docs', db: 'zone', database: 'zone', location: 'ip', gamepad: 'games', robot: 'bot', deploy: 'upload', copy: 'paste', activity: 'ping',
+ // The 2026-10-28 rooms, on the nearest glyph until each has its own (rooms.js): never the fallback mark.
+ domains: 'dns', design: 'theme', today: 'history', report: 'gauge', surf: 'games', casino: 'games', fun: 'quest', meme: 'image', exchange: 'trade', cam: 'video', tours: 'map', vision: 'ai', shoes: 'deals' };
const registry = Object.create(null);
const registry = Object.create(null);
for (const [k, g] of Object.entries(GLYPHS)) registry[k] = { glyph: g };
for (const [k, g] of Object.entries(GLYPHS)) registry[k] = { glyph: g };
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.29.0",
+ "version": "4.29.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
new file mode 100644
new file mode 100644
@@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
+/* openvibe-shared/room.css — the room kit's look (4.29.1; before, in kit.css): a room's accent (rooms.js), kit.card and
+ * kit.rail. A page that shows cards or rails, or wears a room's accent (sc.room() does all three), loads it after
+ * kit.css; pages that use only the kit's controls do not pay for it. Uses kit.css's tokens (--ovk-*). */
+
+/* ── Rooms (rooms.js): the room's accent in the theme's mode. rooms.style(id) sets --room-dark/--room-light on .ov-room;
+ * without them the theme's accent stays. --room-accent: icons, outlines, motifs; --room-strong: text on the page;
+ * --room-fill and --room-on: a filled button and its text (dark text on a dark theme's bright fill, white on light). ── */
+.ov-room { --room-accent: var(--ovk-accent); --room-strong: var(--ovk-accent); --room-fill: var(--ovk-fill); --room-on: var(--ovk-on-fill); }
+.ov-room[style*="--room-dark"] { --room-accent: var(--room-dark); --room-strong: var(--room-dark); --room-fill: var(--room-dark); --room-on: #0b0d12; }
+:root[data-theme-mode="light"] .ov-room[style*="--room-light"] { --room-accent: var(--room-light); --room-strong: var(--room-light); --room-fill: var(--room-light); --room-on: #fff; }
+
+/* ── Cards (kit.card): one card for every kind of thing ── */
+.ovk-card { position: relative; display: flex; flex-direction: column; gap: 10px; min-width: 0; color: var(--ovk-text); }
+.ovk-card-media { position: relative; display: block; aspect-ratio: var(--ovk-ar, 16/9); border-radius: var(--ovk-radius); overflow: hidden; background: var(--ovk-panel-2); isolation: isolate; box-shadow: 0 0 0 1px color-mix(in srgb, var(--ovk-line) 70%, transparent) inset; }
+.ovk-card-media > img, .ovk-card-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.ovk-card-media > img { transition: transform .5s cubic-bezier(.2, .7, .2, 1); }
+.ovk-card-video { opacity: 0; transition: opacity .25s; }
+.ovk-card.is-playing .ovk-card-video { opacity: 1; }
+.ovk-card-noimg { position: absolute; inset: 0; display: grid; place-items: center; color: var(--room-accent, var(--ovk-accent)); background: radial-gradient(120% 90% at 28% 18%, color-mix(in srgb, var(--room-accent, var(--ovk-accent)) 24%, transparent), transparent 72%), var(--ovk-panel-2); }
+.ovk-card-noimg .ovk-i { width: 30%; max-width: 60px; height: auto; aspect-ratio: 1; stroke-width: 1.5; }
+.ovk-card-badge { position: absolute; top: 8px; left: 8px; z-index: 1; display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 6px; background: rgba(0, 0, 0, .74); color: #fff; font: 800 11.5px/1.5 var(--ovk-font); letter-spacing: .04em; }
+.ovk-card-badge.is-live { background: #d4202b; }
+.ovk-card-badge.is-live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #fff; animation: ovk-pulse 1.6s ease-in-out infinite; }
+@keyframes ovk-pulse { 50% { opacity: .35; } }
+.ovk-card-corner { position: absolute; right: 8px; bottom: 8px; z-index: 1; padding: 2px 7px; border-radius: 6px; background: rgba(0, 0, 0, .78); color: #fff; font: 700 12px/1.4 var(--ovk-font); font-variant-numeric: tabular-nums; transition: opacity .25s; }
+.ovk-card-peek { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; padding: 34px 12px 10px; background: linear-gradient(transparent, rgba(0, 0, 0, .88)); color: #fff; font-size: 12.5px; line-height: 1.4; opacity: 0; transform: translateY(8px); transition: opacity .3s, transform .35s cubic-bezier(.2, .7, .2, 1); pointer-events: none; }
+.ovk-card-body { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }
+.ovk-card-body > .ovk-mark { flex: none; }
+.ovk-card-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
+.ovk-card-title { margin: 0; font-size: 15px; font-weight: 700; line-height: 1.3; letter-spacing: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
+.ovk-card-link { color: inherit; text-decoration: none; transition: color .2s; }
+.ovk-card-link::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: var(--ovk-radius); }
+.ovk-card-link:focus-visible { outline: none; }
+.ovk-card-link:focus-visible::after { outline: 2px solid var(--room-accent, var(--ovk-accent)); outline-offset: 3px; }
+.ovk-card-meta { margin: 0; display: flex; flex-wrap: wrap; color: var(--ovk-muted); font-size: 13px; line-height: 1.45; min-width: 0; }
+.ovk-card-meta > * + *::before { content: "·"; margin: 0 6px; }
+.ovk-card-who { font-weight: 600; color: var(--text-secondary, var(--ovk-muted)); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ovk-card .ovk-more { position: relative; z-index: 2; margin: -6px -8px 0 0; }
+@media (hover: hover) {
+ .ovk-card:hover .ovk-card-media > img, .ovk-card:focus-within .ovk-card-media > img { transform: scale(1.05); }
+ .ovk-card:hover .ovk-card-peek, .ovk-card:focus-within .ovk-card-peek { opacity: 1; transform: none; }
+ .ovk-card:hover .ovk-card-corner:has(~ .ovk-card-peek), .ovk-card:focus-within .ovk-card-corner:has(~ .ovk-card-peek) { opacity: 0; }
+ .ovk-card:hover .ovk-card-link { color: var(--room-strong, var(--ovk-accent)); }
+ .ovk-card .ovk-more:not([open]) > summary { opacity: 0; transition: opacity .15s; }
+ .ovk-card:hover .ovk-more > summary, .ovk-card:focus-within .ovk-more > summary { opacity: 1; }
+}
+
+/* ── Rails (kit.rail): a row that scrolls sideways and snaps ── */
+.ovk-rail { margin: 26px 0 0; min-width: 0; --ovk-rail-bleed: 0px; }
+.ovk-rail-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 12px; }
+.ovk-rail-head h2 { margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.01em; }
+.ovk-rail-more { display: inline-flex; align-items: center; gap: 2px; min-height: 32px; color: var(--room-strong, var(--ovk-accent)); font-weight: 700; font-size: .9rem; text-decoration: none; white-space: nowrap; }
+.ovk-rail-more:hover { text-decoration: underline; }
+.ovk-rail-wrap { position: relative; }
+.ovk-rail-track { list-style: none; margin: 0 calc(-1 * var(--ovk-rail-bleed)); padding: 2px var(--ovk-rail-bleed) 10px; display: grid; grid-auto-flow: column; grid-auto-columns: var(--ovk-rail-w); gap: 14px; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: var(--ovk-rail-bleed); overscroll-behavior-x: contain; scrollbar-width: none; }
+.ovk-rail-track::-webkit-scrollbar { display: none; }
+.ovk-rail-track > li { scroll-snap-align: start; min-width: 0; }
+.ovk-rail--m { --ovk-rail-w: min(80%, 320px); }
+.ovk-rail--s { --ovk-rail-w: min(44%, 200px); }
+.ovk-rail--l { --ovk-rail-w: min(86%, 440px); }
+.ovk-rail-btn { position: absolute; top: 34%; z-index: 3; width: 40px; height: 40px; margin-top: -20px; border-radius: 50%; display: grid; place-items: center; border: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); box-shadow: 0 6px 18px rgba(0, 0, 0, .3); cursor: pointer; font-size: 18px; }
+.ovk-rail-btn:hover { border-color: var(--room-accent, var(--ovk-accent)); color: var(--room-strong, var(--ovk-accent)); }
+.ovk-rail-btn:focus-visible { outline: 2px solid var(--room-accent, var(--ovk-accent)); outline-offset: 2px; }
+.ovk-rail-btn[hidden] { display: none; }
+.ovk-rail-prev { left: 6px; }
+.ovk-rail-next { right: 6px; }
+.ovk-pop.is-fixed { position: fixed; right: auto; top: auto; }
+
+@media (max-width: 599px) {
+ .ovk-rail { --ovk-rail-bleed: 16px; }
+ .ovk-rail-btn { display: none; }
+}
+@media (min-width: 600px) {
+ .ovk-rail--m { --ovk-rail-w: 280px; } .ovk-rail--s { --ovk-rail-w: 190px; } .ovk-rail--l { --ovk-rail-w: 400px; }
+}
+@media (prefers-reduced-motion: reduce) { .ovk-card-media > img, .ovk-card-peek, .ovk-card-video { transition: none; } .ovk-card-badge.is-live::before { animation: none; } }
+html[data-ov-motion=reduced] .ovk-card-badge.is-live::before { animation: none; }
@@ -354,7 +354,7 @@ function roomMotif(r) {
@@ -354,7 +354,7 @@ function roomMotif(r) {
* scenes: [{ title, text, html (trusted: the room's own component in a demo state), action: { href, label } }]
* scenes: [{ title, text, html (trusted: the room's own component in a demo state), action: { href, label } }]
* explore: { title, items: [{ href, title, text, icon }] } — sc.features · dev: sc.code's arguments, with links
* explore: { title, items: [{ href, title, text, icon }] } — sc.features · dev: sc.code's arguments, with links
* faq: { title, items: [[q, a]] } · network: sc.network's arguments · html: trusted markup after the scenes
* faq: { title, items: [[q, a]] } · network: sc.network's arguments · html: trusted markup after the scenes
- * Needs kit.css (cards, rails, the room's accent) and showcase.css; motion.js makes the numbers tick and the box type.
+ * Needs kit.css, room.css (cards, rails, the room's accent) and showcase.css; motion.js makes the numbers tick and the box type.
*/
*/
function room({ room: key, hero: h = {}, proof, rails, scenes, explore, dev, faq: q, network: net, html } = {}) {
function room({ room: key, hero: h = {}, proof, rails, scenes, explore, dev, faq: q, network: net, html } = {}) {
roomsLib = roomsLib || require('./rooms');
roomsLib = roomsLib || require('./rooms');
@@ -98,9 +98,13 @@ const sc = require('../showcase');
@@ -98,9 +98,13 @@ const sc = require('../showcase');
// ── The CSS is served and carries the room accent rules; motion.js keeps the server's end state ──
// ── The CSS is served and carries the room accent rules; motion.js keeps the server's end state ──
{
{
+ const rcss = fs.readFileSync(path.join(__dirname, '..', 'room.css'), 'utf8');
const kcss = fs.readFileSync(path.join(__dirname, '..', 'kit.css'), 'utf8');
const kcss = fs.readFileSync(path.join(__dirname, '..', 'kit.css'), 'utf8');
- assert.ok(kcss.includes(':root[data-theme-mode="light"] .ov-room[style*="--room-light"]'), 'light themes take the light accent');
- assert.ok(kcss.includes('.ovk-rail-btn { display: none; }'), 'phones swipe rails');
+ assert.ok(rcss.includes(':root[data-theme-mode="light"] .ov-room[style*="--room-light"]'), 'light themes take the light accent');
+ assert.ok(rcss.includes('.ovk-rail-btn { display: none; }'), 'phones swipe rails');
+ assert.ok(!/ovk-card|ovk-rail|ov-room/.test(kcss), 'kit.css stays the controls only: cards, rails and accents are room.css (4.29.1)');
+ const files = require('../files');
+ for (const f of ['motion.js', 'rooms.js', 'room.css']) assert.ok(files.isBrowserFile(f), `${f} is served at /shared/`);
const scss = fs.readFileSync(path.join(__dirname, '..', 'showcase.css'), 'utf8');
const scss = fs.readFileSync(path.join(__dirname, '..', 'showcase.css'), 'utf8');
assert.ok(scss.includes('.sc-room-hero{') && scss.includes('html.ov-motion [data-ov-reveal]'), 'showcase.css is rebuilt (npm run build:styles)');
assert.ok(scss.includes('.sc-room-hero{') && scss.includes('html.ov-motion [data-ov-reveal]'), 'showcase.css is rebuilt (npm run build:styles)');
const motion = fs.readFileSync(path.join(__dirname, '..', 'motion.js'), 'utf8');
const motion = fs.readFileSync(path.join(__dirname, '..', 'motion.js'), 'utf8');