openvibers/openvibe.design Commits

Show progress and uploader in Design (#207)

OP OpenVibers committed 4 days ago · dd4cc74d4b · 7 files, +99 −0

* Document shared progress and uploader components

* Merge remote-tracking branch 'origin/main' into feat/upload-kit-1109

# Conflicts:
#	package-lock.json
#	package.json

Changes

ddiff --git a/public/css/upload-kit.css b/public/css/upload-kit.css
nnew file mode 100644
iindex 0000000..aa038c0
---- /dev/null
++++ b/public/css/upload-kit.css
@@@ -0,0 +1,11 @@
++.uk-wrap { max-width: 1050px; margin: 0 auto; padding: 20px 20px 70px; }
++.uk-wrap header { max-width: 760px; margin: 14px 0 30px; }
++.uk-wrap h1 { font-size: clamp(2rem, 5vw, 3rem); line-height: 1.1; }
++.uk-wrap p { line-height: 1.6; }
++.uk-kicker { color: var(--ov-accent, #9b7bff); font-weight: 800; text-transform: uppercase; letter-spacing: .12em; }
++.uk-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr)); gap: 15px; }
++.uk-card { min-width: 0; margin: 0 0 16px; padding: 20px; border: 1px solid var(--ov-line, #596179); border-radius: 16px; background: var(--ov-surface, rgba(100,110,145,.08)); }
++.uk-card h2 { margin: 0 0 6px; font-size: 1.2rem; }
++.uk-card > p { margin: 0 0 16px; }
++.uk-card code { overflow-wrap: anywhere; }
++.uk-note { margin-top: 12px !important; opacity: .7; font-size: .9rem; }
ddiff --git a/public/js/upload-kit.js b/public/js/upload-kit.js
nnew file mode 100644
iindex 0000000..81b066c
---- /dev/null
++++ b/public/js/upload-kit.js
@@@ -0,0 +1,16 @@
++(function () {
++ 'use strict';
++ if (!window.OVUpload) return;
++ document.querySelectorAll('[data-demo-state]').forEach(function (el) {
++ var state = el.dataset.demoState;
++ var meter = OVUpload.progress(el, { onRetry: function () { meter.update({ state: 'sending', done: 12 * 1024 * 1024, total: 32 * 1024 * 1024 }); } });
++ meter.update({ state: state, done: state === 'done' || state === 'processing' ? 32 * 1024 * 1024 : state === 'waiting' ? 0 : 12 * 1024 * 1024,
++ total: 32 * 1024 * 1024, message: state === 'failed' ? 'The connection timed out.' : state === 'processing' ? 'Making thumbnails' : '' });
++ if (state === 'sending') {
++ var bytes = 12 * 1024 * 1024;
++ setInterval(function () { bytes = bytes >= 30 * 1024 * 1024 ? 12 * 1024 * 1024 : bytes + 1024 * 1024; meter.update({ state: 'sending', done: bytes }); }, 1100);
++ }
++ });
++ var demo = document.querySelector('[data-demo-picker]');
++ if (demo) OVUpload.picker(demo, { label: 'Choose a file', multiple: true });
++})();
ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex 55c8a08..344beb2 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -38,6 +38,8 @@ function publicPages(themes = []) {
{ path: '/tokens', changefreq: 'weekly', priority: 0.8, title: 'Your theme, as code', text: 'Any OpenVibe theme as design tokens: CSS variables, Tailwind, Jetpack Compose, SwiftUI, Flutter, Figma variables and W3C DTCG JSON, light and dark together.' },
{ path: '/brand', changefreq: 'monthly', priority: 0.7, title: 'The OpenVibe mark', text: 'The OpenVibe mark, a ring O and a round V: what each of its moves means, its forms, colours and the names beside it, with the SVG files and the code to use it on any site.' },
{ path: '/loading', changefreq: 'monthly', priority: 0.6, title: 'Loading: nothing spins', text: 'How OpenVibe waits: placeholders shaped like the content (kit.skeleton), the mark at work instead of a spinner (OpenVibeLoading), the navbar while a page moves (OpenVibeNavbar.busy), and the rules.' },
++ { path: '/progress', changefreq: 'monthly', priority: 0.6, title: 'Upload progress', text: 'The shared ov-progress component: transfer, processing, pause, retry and completion, with tokens and code.' },
++ { path: '/uploader', changefreq: 'monthly', priority: 0.6, title: 'Uploader', text: 'The shared file picker and upload queue for OpenVibe sites, with preview, Remove and code.' },
{ path: '/forms', changefreq: 'monthly', priority: 0.6, title: 'Forms: OpenVibe’s own controls', text: 'OpenVibe’s select and form controls on every site: a menu on a computer, a sheet on a phone, choices that say whether they are ready, switches, fields and choice cards (openvibe-shared ov-forms).' },
{ path: '/native', changefreq: 'monthly', priority: 0.6, title: 'Apps for Android and iOS', text: 'How an OpenVibe page becomes a real Android or iOS screen: one set of tokens, components, patterns and icons, three size classes, what each platform does its own way, and the road from the web to apps.' },
{ path: '/motion', changefreq: 'monthly', priority: 0.6, title: 'Motion', text: 'How OpenVibe moves: the mark\'s motion and one shared kit of effects (glint, glow, lift, press, ripple, tilt, reveal, count, confetti), the timing every site shares, the rules and the code (openvibe-shared/ov-fx).' },
@@@ -104,6 +106,8 @@ function createDiscoveryRoutes(ctx) {
{ title: 'Open font library', url: abs('/fonts'), note: 'Open licence typefaces with live previews, every style, licence, source, CSS and download. Search and filters at /fonts; JSON at GET /api/v1/fonts and /api/v1/fonts/<family>; MCP tools list and get fonts.' },
{ title: 'A free Font Awesome alternative', url: abs('/icons/font-awesome-alternative'), note: 'one script keeps Font Awesome markup, classes, animations and data-fa-transform, and shows the OpenVibe glyphs', html: true },
{ title: 'Loading', url: abs('/loading'), note: 'how OpenVibe waits: placeholders shaped like the content (kit.skeleton: card, row, tile, chip, line, chat, stat), the mark at work instead of a spinner (OpenVibeLoading), the navbar while a page moves (OpenVibeNavbar.busy), and the rules' },
++ { title: 'Progress', url: abs('/progress'), note: 'ov-progress in all six states, with time remaining, reduced motion, tokens and code' },
++ { title: 'Uploader', url: abs('/uploader'), note: 'the shared drop card, file preview, queue and total progress, with code' },
{ title: 'Forms', url: abs('/forms'), note: 'OpenVibe’s own select (a menu on a computer, a sheet on a phone; options with state, badge and a setup link), switches, fields and choice cards: ov-forms.js and ov-forms.css' },
{ title: 'The OpenVibe mark', url: abs('/brand'), note: 'the logo, its states and motion, colours, the brand face, SVG files and code (openvibe-shared/mark, ov-mark.js)' },
{ title: 'The Design Arena', url: abs('/arena'), note: 'one prompt, three AI designers, blind votes and an Elo leaderboard' },
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 8494610..9c52906 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -90,6 +90,8 @@ function createPageRoutes(ctx) {
r.get('/motion', (req, res) => page(res, 200, motionView.motionPage({ base: layoutBase() }), req.viewer));
r.get('/native', (req, res) => page(res, 200, require('../views/native').nativePage(), req.viewer));
r.get('/loading', (req, res) => page(res, 200, require('../views/loading').loadingPage({ base: layoutBase() }), req.viewer));
++ r.get('/progress', (req, res) => page(res, 200, require('../views/upload-kit').progressPage(), req.viewer));
++ r.get('/uploader', (req, res) => page(res, 200, require('../views/upload-kit').uploaderPage(), req.viewer));
// ── OpenVibe's own form controls (openvibe-shared ov-forms, 2026-11-03) ──
r.get('/forms', (req, res) => page(res, 200, require('../views/forms').formsPage({ base: layoutBase() }), req.viewer));
r.get('/patterns', (req, res) => page(res, 200, patternsView.index(), req.viewer));
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex fe6f568..62e6d3a 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -31,6 +31,8 @@ const NAV = [
{ label: 'The mark', href: '/brand', icon: 'fa-star', more: true },
{ label: 'Motion', href: '/motion', icon: 'fa-wand-magic-sparkles', more: true },
{ label: 'Loading', href: '/loading', icon: 'fa-hourglass-half', more: true },
++ { label: 'Progress', href: '/progress', icon: 'fa-bars-progress', more: true },
++ { label: 'Uploader', href: '/uploader', icon: 'fa-upload', more: true },
{ label: 'Forms', href: '/forms', icon: 'fa-list-check', more: true },
{ label: 'Icons', href: '/icons', icon: 'fa-shapes', more: true },
{ label: 'Fonts', href: '/fonts', icon: 'fa-font', more: true },
ddiff --git a/server/views/upload-kit.js b/server/views/upload-kit.js
nnew file mode 100644
iindex 0000000..8aaa449
---- /dev/null
++++ b/server/views/upload-kit.js
@@@ -0,0 +1,41 @@
++'use strict';
++const kit = require('openvibe-shared/kit');
++const { layout } = require('../render/layout');
++
++const styles = ['ov-upload.css'];
++const shared = ['ov-upload.js'];
++const scripts = ['js/upload-kit.js'];
++const states = [
++ ['waiting', 'Waiting', 'A file is ready to send.'],
++ ['sending', 'Sending', 'Bytes, speed and time left update while the file travels.'],
++ ['paused', 'Paused', 'The connection dropped; the upload resumes when it returns.'],
++ ['processing', 'Processing', 'The server is making the file playable.'],
++ ['done', 'Done', 'The check draws when the upload finishes.'],
++ ['failed', 'Failed', 'A plain reason and Retry appear together.'],
++];
++
++function progressPage() {
++ const body = `<article class="uk-wrap"><header><p class="uk-kicker">Components</p><h1>Progress that tells you what is happening</h1>
++<p>One <code>ov-progress</code> follows a file from selection through transfer and processing. It shows bytes, speed and an estimated time, then a drawn check when the file is ready.</p>
++<p><a href="/uploader">See the uploader →</a></p></header>
++<div class="uk-grid">${states.map(([id, name, explanation]) => `<section class="uk-card"><h2>${name}</h2><p>${explanation}</p><ov-progress data-demo-state="${id}"></ov-progress></section>`).join('')}</div>
++<section class="uk-card"><h2>Use it</h2>${kit.snippet({ code: `<link rel="stylesheet" href="/shared/ov-upload.css">\n<script src="/shared/ov-upload.js" defer></script>\n<ov-progress id="transfer"></ov-progress>\n<script>\n const p = OVUpload.progress(document.querySelector('#transfer'), { onRetry: retry });\n p.update({ state: 'sending', done: 8388608, total: 33554432 });\n p.update({ state: 'processing', message: 'Making thumbnails' });\n p.update({ state: 'done' });\n</script>`, id: 'uk-progress-code', label: 'Copy progress code' })}
++<p>States: waiting, sending, paused, processing, done and failed. The server can name each processing step through <code>message</code>.</p>
++<p>Tokens: <code>--ov-accent</code> for focus and Retry, <code>--ov-line</code> for the track and borders, <code>--ov-surface</code> for the card. The gradient is OpenVibe blue, purple and rose. Reduced motion stops the sheen, pulse and check animation.</p></section></article>`;
++ return layout({ title: 'Progress component', description: 'Every state and token of OpenVibe upload progress.', path: '/progress', active: '/progress', body,
++ sharedStyles: styles, shared, scripts, styles: ['css/upload-kit.css'] });
++}
++
++function uploaderPage() {
++ const body = `<article class="uk-wrap"><header><p class="uk-kicker">Components</p><h1>Choose a file and keep going</h1>
++<p>The picker opens the system chooser on a phone and accepts a drop on a computer. A chosen file gets its preview, name, size and Remove control. Sites pass the transfer function that reaches Media.</p>
++<p><a href="/progress">See progress states →</a></p></header>
++<section class="uk-card"><h2>Try the picker</h2><div data-demo-picker></div><p class="uk-note">This example stays on your device; it does not upload the file.</p></section>
++<section class="uk-card"><h2>Use it</h2>${kit.snippet({ code: `<link rel="stylesheet" href="/shared/ov-upload.css">\n<script src="/shared/ov-upload.js" defer></script>\n<div id="files"></div>\n<script>\n OVUpload.queue(document.querySelector('#files'), {\n label: 'Choose files', multiple: true, concurrency: 3,\n upload: (file, { onProgress, signal }) => media.upload(file, { onProgress, signal })\n });\n</script>`, id: 'uk-picker-code', label: 'Copy uploader code' })}
++<p>Use <code>OVUpload.picker</code> for a single file. Use <code>OVUpload.queue</code> for multiple files and a total progress bar. The transport owns retry and resume; Media's multipart API supports both.</p>
++<p>Tokens: <code>--ov-accent</code> for focus, <code>--ov-line</code> for the drop card, <code>--ov-surface</code> for its background. The native input is labelled and visually hidden.</p></section></article>`;
++ return layout({ title: 'Uploader component', description: 'The shared OpenVibe file picker and queue.', path: '/uploader', active: '/uploader', body,
++ sharedStyles: styles, shared, scripts, styles: ['css/upload-kit.css'] });
++}
++
++module.exports = { progressPage, uploaderPage };
ddiff --git a/test/upload-kit-page.test.js b/test/upload-kit-page.test.js
nnew file mode 100644
iindex 0000000..7ff2134
---- /dev/null
++++ b/test/upload-kit-page.test.js
@@@ -0,0 +1,23 @@
++'use strict';
++const assert = require('node:assert/strict');
++const { boot, check, done } = require('./helpers/boot');
++
++(async () => {
++ const t = await boot();
++ try {
++ await check('Progress shows every state and its shared assets', async () => {
++ const page = await t.get('/progress');
++ assert.equal(page.status, 200);
++ for (const state of ['waiting', 'sending', 'paused', 'processing', 'done', 'failed']) assert.ok(page.text.includes(`data-demo-state="${state}"`));
++ assert.ok(page.text.includes('/shared/ov-upload.js') && page.text.includes('/shared/ov-upload.css'));
++ assert.ok(page.text.includes('prefers-reduced-motion') || page.text.includes('Reduced motion'));
++ });
++ await check('Uploader has the demo, queue code and discoverable links', async () => {
++ const page = await t.get('/uploader');
++ assert.equal(page.status, 200);
++ assert.ok(page.text.includes('data-demo-picker') && page.text.includes('OVUpload.queue'));
++ assert.ok((await t.get('/sitemap.xml')).text.includes('/progress'));
++ assert.ok((await t.get('/llms.txt')).text.includes('/uploader'));
++ });
++ } finally { await done(t); }
++})();